Merge PR #143: foundation shared types + struct plumbing (#1006)

Reviewed-by: Hoshe (code quality), Tyre (architecture) — both APPROVE.
This commit is contained in:
2026-05-30 22:49:55 +02:00
8 changed files with 692 additions and 40 deletions
+15 -1
View File
@@ -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,
} }
} }
+1
View File
@@ -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,
} }
} }
+15 -5
View File
@@ -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,16 +100,19 @@ 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 (skeleton + block tags) produced by the plan phase (D-230).
/// Boxed to keep `GenCompletion` variant sizes balanced (D-230 skeleton is ~2.7 KB).
state: Box<DistrictWorldState>,
}, },
ChunkFilled { ChunkFilled {
district_id: u64, district_id: u64,
block_pos: (u32, u32), block_pos: (u32, u32),
}, },
/// Work item failed — body_id or city_id for logging. /// Work item failed — body_id or city_id for logging.
Failed { Failed { item: GenWorkItem, reason: String },
item: GenWorkItem,
reason: String,
},
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -336,7 +340,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: Box::new(DistrictWorldState::default()),
}
} }
GenWorkItem::FillChunk { GenWorkItem::FillChunk {
district_id, district_id,
+1
View File
@@ -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();
+29 -2
View File
@@ -80,8 +80,35 @@ 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) {
// Key by state.skeleton.district_id (D-194/D-230): a city has many
// districts, each with its own DistrictId. `city_id` is only the
// dispatch key used in the work item — the canonical insert key is
// the district's own stable id. TODO(#957): the stub GenerateSkeleton
// returns a default skeleton with district_id=0; real gen (#957) will
// populate it from CityGenerationContext.
let _ = city_id; // used as dispatch key only; district_id is the map key
body_state
.districts
.insert(state.skeleton.district_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 { .. } => {}
} }
} }
} }
+7 -1
View File
@@ -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,
} }
} }
+3 -1
View File
@@ -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,
+621 -30
View File
@@ -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};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -101,7 +103,7 @@ pub type PlacedObject = String;
/// Determines simulation fidelity budget and NPC complexity ceiling. /// Determines simulation fidelity budget and NPC complexity ceiling.
/// ///
/// Source: D-218, workshop-outcomes.md /// Source: D-218, workshop-outcomes.md
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum WorldTier { pub enum WorldTier {
/// Hub system. Full simulation, high faction pressure. /// Hub system. Full simulation, high faction pressure.
Epicenter, Epicenter,
@@ -112,12 +114,13 @@ pub enum WorldTier {
/// Transit stop. Pass-through node. Moderate complexity ceiling. /// Transit stop. Pass-through node. Moderate complexity ceiling.
Passage, Passage,
/// Not simulated until player approaches. Minimal complexity ceiling. /// Not simulated until player approaches. Minimal complexity ceiling.
#[default]
Waypoint, Waypoint,
} }
/// Generator content budget for a district. /// Generator content budget for a district.
/// Derived from WorldTier + SettingType at Phase 1. /// Derived from WorldTier + SettingType at Phase 1.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum ComplexityTier { pub enum ComplexityTier {
/// Full social architecture. All Tier 1+2 guarantees. 20-80+ NPCs. /// Full social architecture. All Tier 1+2 guarantees. 20-80+ NPCs.
Full, Full,
@@ -126,26 +129,34 @@ pub enum ComplexityTier {
/// Minimal social architecture. Tier 1 only. 1-8 NPCs. /// Minimal social architecture. Tier 1 only. 1-8 NPCs.
Minimal, Minimal,
/// No social architecture. Pure terrain. 0 NPCs. No guarantees. /// No social architecture. Pure terrain. 0 NPCs. No guarantees.
#[default]
Empty, Empty,
} }
/// Physical setting type for a district. /// Physical setting type for a district.
/// Merged from Gestalt's SettingGeometry and Tyre's TerrainType. /// Merged from Gestalt's SettingGeometry and Tyre's TerrainType.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum SettingType { pub enum SettingType {
Station, Station,
#[default]
Urban, Urban,
Agricultural, Agricultural,
Maritime, Maritime,
Wilderness { biome: Biome }, Wilderness {
Water { water_type: WaterType }, biome: Biome,
},
Water {
water_type: WaterType,
},
Transitional, Transitional,
Orbital, Orbital,
Specialized { function: SpecializedFunction }, Specialized {
function: SpecializedFunction,
},
} }
/// Classification of a district (high-level function). /// Classification of a district (high-level function).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum DistrictType { pub enum DistrictType {
LogisticsHub, LogisticsHub,
Residential, Residential,
@@ -153,15 +164,17 @@ pub enum DistrictType {
Industrial, Industrial,
Administrative, Administrative,
Entertainment, Entertainment,
#[default]
MixedUse, MixedUse,
Transit, Transit,
Specialized, Specialized,
} }
/// How blocks are placed within the district's 512×512 footprint. /// How blocks are placed within the district's 512×512 footprint.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub enum DistrictLayoutMode { pub enum DistrictLayoutMode {
/// Standard Cartesian grid — perpendicular streets. /// Standard Cartesian grid — perpendicular streets.
#[default]
Grid, Grid,
/// Organic placement with per-block offsets and rotations. /// Organic placement with per-block offsets and rotations.
Organic { Organic {
@@ -170,7 +183,7 @@ pub enum DistrictLayoutMode {
} }
/// Zoning classification for a block or floor zone. /// Zoning classification for a block or floor zone.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum ZoningType { pub enum ZoningType {
Commercial, Commercial,
Residential, Residential,
@@ -179,11 +192,12 @@ pub enum ZoningType {
Transit, Transit,
Recreational, Recreational,
Restricted, Restricted,
#[default]
Mixed, Mixed,
} }
/// Reservation function — what purpose a multi-block reservation serves. /// Reservation function — what purpose a multi-block reservation serves.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ReservationFunction { pub enum ReservationFunction {
Skyscraper, Skyscraper,
Park, Park,
@@ -196,9 +210,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.
@@ -212,7 +229,7 @@ pub enum AccessTier {
} }
/// Vertical corridor type (how floors connect in a multi-level reservation). /// Vertical corridor type (how floors connect in a multi-level reservation).
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum VerticalCorridorType { pub enum VerticalCorridorType {
Stairwell, Stairwell,
Elevator, Elevator,
@@ -281,7 +298,7 @@ pub enum WallBackside {
} }
/// Era cause — why a block has the era tag it has. /// Era cause — why a block has the era tag it has.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum EraCause { pub enum EraCause {
Original, Original,
CorporateMerger, CorporateMerger,
@@ -333,7 +350,7 @@ pub enum PoliticalArchetype {
/// Primary spatial axis of a city's original street grid. /// Primary spatial axis of a city's original street grid.
/// Derived from the matched attractor type (D-211). Controls district grid rotation. /// Derived from the matched attractor type (D-211). Controls district grid rotation.
/// Source: D-213 /// Source: D-213
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum FoundingOrientation { pub enum FoundingOrientation {
/// Street grid perpendicular to coastline. `facing_degrees`: compass bearing toward water (0–359). /// Street grid perpendicular to coastline. `facing_degrees`: compass bearing toward water (0–359).
Coastal { facing_degrees: u16 }, Coastal { facing_degrees: u16 },
@@ -445,11 +462,473 @@ 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, 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 /// 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
#[derive(Serialize, Deserialize, Clone, Debug)] ///
/// Amended by D-229/D-232/D-233: adds `morphology_zone`, `trait_selection`,
/// `dominant_bulk_class`, `dominant_production_ubiquity`.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct CityGenerationContext { pub struct CityGenerationContext {
/// Foreign key into atlas_city_names.id /// Foreign key into atlas_city_names.id
pub city_id: u64, pub city_id: u64,
@@ -463,6 +942,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,
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -470,7 +961,7 @@ pub struct CityGenerationContext {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Organic layout placement for a single block. /// Organic layout placement for a single block.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct BlockPlacement { pub struct BlockPlacement {
/// Offset from grid-aligned position (±16 sim tiles per axis max). /// Offset from grid-aligned position (±16 sim tiles per axis max).
pub offset: (i16, i16), pub offset: (i16, i16),
@@ -483,7 +974,7 @@ pub struct BlockPlacement {
/// Single block within a district's 4×4 block grid. /// Single block within a district's 4×4 block grid.
/// Each block = 128×128 sim tiles = 2×2 chunks. /// Each block = 128×128 sim tiles = 2×2 chunks.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub struct BlockSkeleton { pub struct BlockSkeleton {
/// Grid position (0–3, 0–3). /// Grid position (0–3, 0–3).
pub position: (u8, u8), pub position: (u8, u8),
@@ -506,36 +997,36 @@ pub struct BlockSkeleton {
/// **D-110:** `z_level` is `i8` — negative values represent basements /// **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` /// (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). /// on the parent reservation which counts total floors (always ≥ 1).
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct FloorZone { pub struct FloorZone {
/// Absolute z-level of this floor. Negative for sub-ground levels (D-110). /// Absolute z-level of this floor. Negative for sub-ground levels (D-110).
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.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct VerticalCorridorSpec { pub struct VerticalCorridorSpec {
/// Which blocks this vertical corridor passes through. /// Which blocks this vertical corridor passes through.
pub block_coords: Vec<(u8, u8)>, pub block_coords: Vec<(u8, u8)>,
/// 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,
} }
/// Visual palette for a zone — base material + contextual modifiers. /// Visual palette for a zone — base material + contextual modifiers.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct ZonePalette { pub struct ZonePalette {
pub base: BasePalette, pub base: BasePalette,
pub modifiers: Vec<PaletteModifier>, pub modifiers: Vec<PaletteModifier>,
} }
/// Contextual modifier applied on top of a base palette. /// Contextual modifier applied on top of a base palette.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum PaletteModifier { pub enum PaletteModifier {
EconomicFunction(EconomicModifier), EconomicFunction(EconomicModifier),
Era(Era), Era(Era),
@@ -545,20 +1036,20 @@ pub enum PaletteModifier {
} }
/// Social site placement within a district. /// Social site placement within a district.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct SocialSitePlacement { pub struct SocialSitePlacement {
pub site_id: SocialSiteId, pub site_id: SocialSiteId,
/// 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>,
} }
/// Triangle assignment within a social site. /// Triangle assignment within a social site.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct TriangleAssignment { pub struct TriangleAssignment {
pub template: TriangleTemplate, pub template: TriangleTemplate,
pub purposes: Vec<TrianglePurpose>, pub purposes: Vec<TrianglePurpose>,
@@ -611,7 +1102,7 @@ pub struct StructuralChange {
/// `base_z: -3, z_levels: 24` /// `base_z: -3, z_levels: 24`
/// A deep mine shaft descending 30 floors below ground: /// A deep mine shaft descending 30 floors below ground:
/// `base_z: -30, z_levels: 30` /// `base_z: -30, z_levels: 30`
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct MultiBlockReservation { pub struct MultiBlockReservation {
/// Which blocks (grid positions) are part of this reservation. /// Which blocks (grid positions) are part of this reservation.
pub blocks: Vec<(u8, u8)>, pub blocks: Vec<(u8, u8)>,
@@ -653,7 +1144,7 @@ pub struct MultiBlockReservation {
/// ///
/// All `Vec<_>` fields are stable-ordered at generation time (sorted by a /// All `Vec<_>` fields are stable-ordered at generation time (sorted by a
/// deterministic key). Generation reproduces identical output for the same seed. /// deterministic key). Generation reproduces identical output for the same seed.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub struct DistrictSkeleton { pub struct DistrictSkeleton {
// ── Identity ────────────────────────────────────────── // ── Identity ──────────────────────────────────────────
pub district_id: DistrictId, pub district_id: DistrictId,
@@ -703,6 +1194,106 @@ pub struct DistrictSkeleton {
mod tests { mod tests {
use super::*; 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. /// D-110: FloorZone.z_level must be i8 (signed) to support negative sub-levels.
#[test] #[test]
fn floor_zone_z_level_is_signed() { fn floor_zone_z_level_is_signed() {
@@ -713,7 +1304,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");
} }