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:
2026-05-30 16:07:33 +02:00
co-authored by Claude Opus 4.8
parent 6e7f6356f0
commit 46b8688fa3
10 changed files with 535 additions and 12 deletions
+15 -1
View File
@@ -13,7 +13,7 @@ use std::collections::{BTreeMap, BTreeSet};
use bevy_ecs::prelude::Resource;
use serde::{Deserialize, Serialize};
use crate::simulation::generator::GeographicAttractor;
use crate::simulation::generator::{DistrictId, DistrictWorldState, GeographicAttractor};
/// Simulation tick counter — monotonically increasing u64.
pub type SimTick = u64;
@@ -71,6 +71,11 @@ pub struct BodyWorldState {
pub drainage_basins: Vec<DrainageBasin>,
/// Geographic attractors (D-195, D-209). Empty until attractor task completes.
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.
pub last_accessed: SimTick,
}
@@ -129,6 +134,14 @@ impl BodyWorldStateCache {
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`.
pub fn contains(&self, body_id: &str) -> bool {
self.entries.contains_key(body_id)
@@ -183,6 +196,7 @@ mod tests {
river_network: RiverNetwork::default(),
drainage_basins: vec![],
attractors: vec![],
districts: BTreeMap::new(),
last_accessed: tick,
}
}
+1
View File
@@ -69,6 +69,7 @@ impl CascadeSnapshot {
river_network,
drainage_basins,
attractors,
districts: std::collections::BTreeMap::new(),
last_accessed: 0,
}
}
+13 -1
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::heightmap::{load_heightmap_png, GRID_H, GRID_W};
use crate::seed::SeedChain;
use crate::simulation::generator::DistrictWorldState;
// ---------------------------------------------------------------------------
// Priority
@@ -99,6 +100,11 @@ pub enum GenCompletion {
},
SkeletonGenerated {
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 {
district_id: u64,
@@ -336,7 +342,13 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
},
},
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 {
district_id,
+1
View File
@@ -200,6 +200,7 @@ mod tests {
river_network: RiverNetwork::default(),
drainage_basins: vec![],
attractors: vec![],
districts: std::collections::BTreeMap::new(),
last_accessed: 0,
});
let (_db, resolver) = empty_resolver();
+20 -2
View File
@@ -80,8 +80,26 @@ fn drain_generation_completions(
GenCompletion::Failed { item, reason } => {
tracing::warn!(?item, %reason, "background generation work item failed");
}
// Produced once the later layers land (#957 / #959); no consumer yet.
GenCompletion::SkeletonGenerated { .. } | GenCompletion::ChunkFilled { .. } => {}
// Insert district world state into the matching body's cache entry (D-230).
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 { .. } => {}
}
}
}
+7 -1
View File
@@ -411,7 +411,8 @@ fn derive_reservations(
mod tests {
use super::*;
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 {
@@ -424,6 +425,11 @@ mod tests {
footprint_radius_km: 10.0,
founding_orientation: FoundingOrientation::Cardinal,
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.
use serde::{Deserialize, Serialize};
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).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum TileCondition {
/// prosperity_score > 0.63. Clean, undamaged, well-maintained.
Intact,