- 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>
This commit is contained in:
Generated
-4
@@ -1492,7 +1492,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_norway",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"sysinfo",
|
||||
"thiserror",
|
||||
"toml",
|
||||
@@ -1552,9 +1551,6 @@ name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
|
||||
@@ -31,7 +31,6 @@ bytemuck = "1"
|
||||
png = "0.17"
|
||||
toml = "0.8"
|
||||
aho-corasick = "1"
|
||||
smallvec = { version = "1", features = ["serde"] }
|
||||
# Economics simulation — Leontief + tâtonnement + D-180 event port (#821)
|
||||
econ-sim = { path = "../tooling/econ-sim" }
|
||||
|
||||
|
||||
@@ -103,8 +103,9 @@ pub enum GenCompletion {
|
||||
/// 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,
|
||||
/// 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 {
|
||||
district_id: u64,
|
||||
@@ -344,7 +345,7 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
|
||||
GenCompletion::SkeletonGenerated {
|
||||
city_id: *city_id,
|
||||
body_id: String::new(),
|
||||
state: DistrictWorldState::default(),
|
||||
state: Box::new(DistrictWorldState::default()),
|
||||
}
|
||||
}
|
||||
GenWorkItem::FillChunk {
|
||||
|
||||
@@ -88,9 +88,16 @@ fn drain_generation_completions(
|
||||
} => {
|
||||
if !body_id.is_empty() {
|
||||
if let Some(body_state) = cache.peek_mut(&body_id) {
|
||||
// TODO(#957): keying by city_id is a stub — a city has multiple districts;
|
||||
// use DistrictSkeleton.district_id as the DistrictId key when real skeleton gen lands.
|
||||
body_state.districts.insert(city_id, state);
|
||||
// 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,
|
||||
|
||||
@@ -103,7 +103,7 @@ pub type PlacedObject = String;
|
||||
/// Determines simulation fidelity budget and NPC complexity ceiling.
|
||||
///
|
||||
/// Source: D-218, workshop-outcomes.md
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
pub enum WorldTier {
|
||||
/// Hub system. Full simulation, high faction pressure.
|
||||
Epicenter,
|
||||
@@ -114,12 +114,13 @@ pub enum WorldTier {
|
||||
/// Transit stop. Pass-through node. Moderate complexity ceiling.
|
||||
Passage,
|
||||
/// Not simulated until player approaches. Minimal complexity ceiling.
|
||||
#[default]
|
||||
Waypoint,
|
||||
}
|
||||
|
||||
/// Generator content budget for a district.
|
||||
/// Derived from WorldTier + SettingType at Phase 1.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ComplexityTier {
|
||||
/// Full social architecture. All Tier 1+2 guarantees. 20-80+ NPCs.
|
||||
Full,
|
||||
@@ -128,26 +129,34 @@ pub enum ComplexityTier {
|
||||
/// Minimal social architecture. Tier 1 only. 1-8 NPCs.
|
||||
Minimal,
|
||||
/// No social architecture. Pure terrain. 0 NPCs. No guarantees.
|
||||
#[default]
|
||||
Empty,
|
||||
}
|
||||
|
||||
/// Physical setting type for a district.
|
||||
/// Merged from Gestalt's SettingGeometry and Tyre's TerrainType.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum SettingType {
|
||||
Station,
|
||||
#[default]
|
||||
Urban,
|
||||
Agricultural,
|
||||
Maritime,
|
||||
Wilderness { biome: Biome },
|
||||
Water { water_type: WaterType },
|
||||
Wilderness {
|
||||
biome: Biome,
|
||||
},
|
||||
Water {
|
||||
water_type: WaterType,
|
||||
},
|
||||
Transitional,
|
||||
Orbital,
|
||||
Specialized { function: SpecializedFunction },
|
||||
Specialized {
|
||||
function: SpecializedFunction,
|
||||
},
|
||||
}
|
||||
|
||||
/// Classification of a district (high-level function).
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum DistrictType {
|
||||
LogisticsHub,
|
||||
Residential,
|
||||
@@ -155,15 +164,17 @@ pub enum DistrictType {
|
||||
Industrial,
|
||||
Administrative,
|
||||
Entertainment,
|
||||
#[default]
|
||||
MixedUse,
|
||||
Transit,
|
||||
Specialized,
|
||||
}
|
||||
|
||||
/// How blocks are placed within the district's 512×512 footprint.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
|
||||
pub enum DistrictLayoutMode {
|
||||
/// Standard Cartesian grid — perpendicular streets.
|
||||
#[default]
|
||||
Grid,
|
||||
/// Organic placement with per-block offsets and rotations.
|
||||
Organic {
|
||||
@@ -172,7 +183,7 @@ pub enum DistrictLayoutMode {
|
||||
}
|
||||
|
||||
/// Zoning classification for a block or floor zone.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ZoningType {
|
||||
Commercial,
|
||||
Residential,
|
||||
@@ -181,11 +192,12 @@ pub enum ZoningType {
|
||||
Transit,
|
||||
Recreational,
|
||||
Restricted,
|
||||
#[default]
|
||||
Mixed,
|
||||
}
|
||||
|
||||
/// Reservation function — what purpose a multi-block reservation serves.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ReservationFunction {
|
||||
Skyscraper,
|
||||
Park,
|
||||
@@ -217,7 +229,7 @@ pub enum ZoneAccessTier {
|
||||
}
|
||||
|
||||
/// Vertical corridor type (how floors connect in a multi-level reservation).
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum VerticalCorridorType {
|
||||
Stairwell,
|
||||
Elevator,
|
||||
@@ -286,7 +298,7 @@ pub enum WallBackside {
|
||||
}
|
||||
|
||||
/// Era cause — why a block has the era tag it has.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum EraCause {
|
||||
Original,
|
||||
CorporateMerger,
|
||||
@@ -338,7 +350,7 @@ pub enum PoliticalArchetype {
|
||||
/// Primary spatial axis of a city's original street grid.
|
||||
/// Derived from the matched attractor type (D-211). Controls district grid rotation.
|
||||
/// Source: D-213
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FoundingOrientation {
|
||||
/// Street grid perpendicular to coastline. `facing_degrees`: compass bearing toward water (0–359).
|
||||
Coastal { facing_degrees: u16 },
|
||||
@@ -534,7 +546,7 @@ impl Default for FloorHeightProfile {
|
||||
|
||||
/// 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)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct FloorExtent {
|
||||
/// Index of the bottom floor (negative = basement, per D-110).
|
||||
pub base_floor: i8,
|
||||
@@ -682,7 +694,7 @@ pub struct ArchitectureFlavorRef {
|
||||
///
|
||||
/// 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)]
|
||||
#[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,
|
||||
@@ -744,7 +756,7 @@ pub enum DoorInitialState {
|
||||
}
|
||||
|
||||
/// Door credential requirement (D-231).
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DoorCredential {
|
||||
None,
|
||||
/// Door is only accessible during specified hours (0–23, inclusive range).
|
||||
@@ -769,7 +781,7 @@ pub enum DoorCredential {
|
||||
}
|
||||
|
||||
/// What a door connects to on the other side (D-231).
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DoorConnectsTo {
|
||||
/// Opens onto a named street.
|
||||
Street { street_id: String },
|
||||
@@ -783,7 +795,7 @@ pub enum DoorConnectsTo {
|
||||
///
|
||||
/// A future interior generator produces a deterministic floor plan from this
|
||||
/// descriptor + `SeedChain` with **no other system queried**.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct InteriorDescriptor {
|
||||
pub zone_type_id: ZoneTypeId,
|
||||
pub entry_class: BuildingEntryClass,
|
||||
@@ -805,7 +817,7 @@ pub enum LayoutMode {
|
||||
}
|
||||
|
||||
/// A single door on a building — the step3→step4 boundary (D-231).
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct DoorSpec {
|
||||
/// Which face of the building footprint this door is on.
|
||||
pub facing: CardinalDirection,
|
||||
@@ -897,8 +909,12 @@ pub enum ProductionUbiquity {
|
||||
/// 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)]
|
||||
///
|
||||
/// 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).
|
||||
@@ -912,7 +928,7 @@ pub struct DistrictWorldState {
|
||||
///
|
||||
/// 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, PartialEq)]
|
||||
pub struct CityGenerationContext {
|
||||
/// Foreign key into atlas_city_names.id
|
||||
pub city_id: u64,
|
||||
@@ -945,7 +961,7 @@ pub struct CityGenerationContext {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Organic layout placement for a single block.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct BlockPlacement {
|
||||
/// Offset from grid-aligned position (±16 sim tiles per axis max).
|
||||
pub offset: (i16, i16),
|
||||
@@ -958,7 +974,7 @@ pub struct BlockPlacement {
|
||||
|
||||
/// Single block within a district's 4×4 block grid.
|
||||
/// Each block = 128×128 sim tiles = 2×2 chunks.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
pub struct BlockSkeleton {
|
||||
/// Grid position (0–3, 0–3).
|
||||
pub position: (u8, u8),
|
||||
@@ -981,7 +997,7 @@ pub struct BlockSkeleton {
|
||||
/// **D-110:** `z_level` is `i8` — negative values represent basements
|
||||
/// (e.g. `z_level = -1` for a sub-basement). This differs from `z_levels: u8`
|
||||
/// on the parent reservation which counts total floors (always ≥ 1).
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FloorZone {
|
||||
/// Absolute z-level of this floor. Negative for sub-ground levels (D-110).
|
||||
pub z_level: i8,
|
||||
@@ -991,7 +1007,7 @@ pub struct FloorZone {
|
||||
}
|
||||
|
||||
/// Vertical connection spec within a multi-level reservation.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct VerticalCorridorSpec {
|
||||
/// Which blocks this vertical corridor passes through.
|
||||
pub block_coords: Vec<(u8, u8)>,
|
||||
@@ -1003,14 +1019,14 @@ pub struct VerticalCorridorSpec {
|
||||
}
|
||||
|
||||
/// Visual palette for a zone — base material + contextual modifiers.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct ZonePalette {
|
||||
pub base: BasePalette,
|
||||
pub modifiers: Vec<PaletteModifier>,
|
||||
}
|
||||
|
||||
/// Contextual modifier applied on top of a base palette.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PaletteModifier {
|
||||
EconomicFunction(EconomicModifier),
|
||||
Era(Era),
|
||||
@@ -1020,7 +1036,7 @@ pub enum PaletteModifier {
|
||||
}
|
||||
|
||||
/// Social site placement within a district.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SocialSitePlacement {
|
||||
pub site_id: SocialSiteId,
|
||||
/// Which blocks this site spans.
|
||||
@@ -1033,7 +1049,7 @@ pub struct SocialSitePlacement {
|
||||
}
|
||||
|
||||
/// Triangle assignment within a social site.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TriangleAssignment {
|
||||
pub template: TriangleTemplate,
|
||||
pub purposes: Vec<TrianglePurpose>,
|
||||
@@ -1086,7 +1102,7 @@ pub struct StructuralChange {
|
||||
/// `base_z: -3, z_levels: 24`
|
||||
/// A deep mine shaft descending 30 floors below ground:
|
||||
/// `base_z: -30, z_levels: 30`
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MultiBlockReservation {
|
||||
/// Which blocks (grid positions) are part of this reservation.
|
||||
pub blocks: Vec<(u8, u8)>,
|
||||
@@ -1128,7 +1144,7 @@ pub struct MultiBlockReservation {
|
||||
///
|
||||
/// All `Vec<_>` fields are stable-ordered at generation time (sorted by a
|
||||
/// deterministic key). Generation reproduces identical output for the same seed.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
pub struct DistrictSkeleton {
|
||||
// ── Identity ──────────────────────────────────────────
|
||||
pub district_id: DistrictId,
|
||||
@@ -1178,6 +1194,106 @@ pub struct DistrictSkeleton {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── FloorExtent: floor_at_voxel_z and voxel_range_for_floor ─────────────
|
||||
|
||||
/// (a) Uniform building at ground level — round-trip voxel↔floor.
|
||||
///
|
||||
/// Uniform(3), base_floor=0, floor_count=2 → floors 0,1; heights 3 each.
|
||||
/// Floor 0 occupies voxels 0–2; floor 1 occupies voxels 3–5.
|
||||
#[test]
|
||||
fn floor_extent_uniform_ground_round_trip() {
|
||||
let extent = FloorExtent {
|
||||
base_floor: 0,
|
||||
floor_count: 2,
|
||||
heights: FloorHeightProfile::Uniform(3),
|
||||
};
|
||||
// Range for each floor.
|
||||
assert_eq!(extent.voxel_range_for_floor(0), Some((0, 2)));
|
||||
assert_eq!(extent.voxel_range_for_floor(1), Some((3, 5)));
|
||||
// Round-trip: voxel → floor → range contains voxel.
|
||||
assert_eq!(extent.floor_at_voxel_z(0), Some(0));
|
||||
assert_eq!(extent.floor_at_voxel_z(2), Some(0));
|
||||
assert_eq!(extent.floor_at_voxel_z(3), Some(1));
|
||||
assert_eq!(extent.floor_at_voxel_z(5), Some(1));
|
||||
}
|
||||
|
||||
/// (b) Basement building — base_floor=-1, floor_count=3 (floors -1, 0, 1).
|
||||
///
|
||||
/// Uniform(3): floor -1 → voxels 0–2, floor 0 → 3–5, floor 1 → 6–8.
|
||||
#[test]
|
||||
fn floor_extent_basement_building() {
|
||||
let extent = FloorExtent {
|
||||
base_floor: -1,
|
||||
floor_count: 3,
|
||||
heights: FloorHeightProfile::Uniform(3),
|
||||
};
|
||||
assert_eq!(extent.voxel_range_for_floor(-1), Some((0, 2)));
|
||||
assert_eq!(extent.voxel_range_for_floor(0), Some((3, 5)));
|
||||
assert_eq!(extent.voxel_range_for_floor(1), Some((6, 8)));
|
||||
assert_eq!(extent.floor_at_voxel_z(0), Some(-1));
|
||||
assert_eq!(extent.floor_at_voxel_z(3), Some(0));
|
||||
assert_eq!(extent.floor_at_voxel_z(6), Some(1));
|
||||
assert_eq!(extent.floor_at_voxel_z(8), Some(1));
|
||||
}
|
||||
|
||||
/// (c) Variable([5,3,3]) floor heights — confirm each floor's voxel range.
|
||||
///
|
||||
/// base_floor=0, floor_count=3: floor 0 → 5 voxels (0–4), floor 1 → 3 voxels (5–7),
|
||||
/// floor 2 → 3 voxels (8–10).
|
||||
#[test]
|
||||
fn floor_extent_variable_heights() {
|
||||
let extent = FloorExtent {
|
||||
base_floor: 0,
|
||||
floor_count: 3,
|
||||
heights: FloorHeightProfile::Variable(vec![5, 3, 3]),
|
||||
};
|
||||
assert_eq!(extent.voxel_range_for_floor(0), Some((0, 4)));
|
||||
assert_eq!(extent.voxel_range_for_floor(1), Some((5, 7)));
|
||||
assert_eq!(extent.voxel_range_for_floor(2), Some((8, 10)));
|
||||
assert_eq!(extent.floor_at_voxel_z(0), Some(0));
|
||||
assert_eq!(extent.floor_at_voxel_z(4), Some(0));
|
||||
assert_eq!(extent.floor_at_voxel_z(5), Some(1));
|
||||
assert_eq!(extent.floor_at_voxel_z(7), Some(1));
|
||||
assert_eq!(extent.floor_at_voxel_z(8), Some(2));
|
||||
assert_eq!(extent.floor_at_voxel_z(10), Some(2));
|
||||
}
|
||||
|
||||
/// (d) Boundary — one voxel above the top floor returns None; one below the base returns None.
|
||||
///
|
||||
/// Uniform(3), base_floor=0, floor_count=2: valid range [0, 5].
|
||||
/// voxel_z=6 is one above the top; voxel_z=-1 is one below the base.
|
||||
#[test]
|
||||
fn floor_extent_boundary_returns_none() {
|
||||
let extent = FloorExtent {
|
||||
base_floor: 0,
|
||||
floor_count: 2,
|
||||
heights: FloorHeightProfile::Uniform(3),
|
||||
};
|
||||
// One voxel above the top floor (top floor ends at voxel 5).
|
||||
assert_eq!(
|
||||
extent.floor_at_voxel_z(6),
|
||||
None,
|
||||
"one voxel above top floor must return None"
|
||||
);
|
||||
// One voxel below the base floor (base maps to voxel 0).
|
||||
assert_eq!(
|
||||
extent.floor_at_voxel_z(-1),
|
||||
None,
|
||||
"one voxel below base floor must return None"
|
||||
);
|
||||
// Floor index outside extent also returns None from voxel_range_for_floor.
|
||||
assert_eq!(
|
||||
extent.voxel_range_for_floor(2),
|
||||
None,
|
||||
"floor index beyond floor_count must return None"
|
||||
);
|
||||
assert_eq!(
|
||||
extent.voxel_range_for_floor(-1),
|
||||
None,
|
||||
"floor index below base_floor must return None"
|
||||
);
|
||||
}
|
||||
|
||||
/// D-110: FloorZone.z_level must be i8 (signed) to support negative sub-levels.
|
||||
#[test]
|
||||
fn floor_zone_z_level_is_signed() {
|
||||
|
||||
Reference in New Issue
Block a user