diff --git a/crates/citygen/src/generate/mod.rs b/crates/citygen/src/generate/mod.rs index 549680c..da0df04 100644 --- a/crates/citygen/src/generate/mod.rs +++ b/crates/citygen/src/generate/mod.rs @@ -6,6 +6,7 @@ pub mod districts; pub mod dressing; pub mod interiors; pub mod lots; +pub mod naming; pub mod streets; pub mod terrain; pub mod verticals; @@ -237,6 +238,7 @@ pub(crate) fn runner_for(stage: Stage) -> Box { Stage::Interiors => Box::new(interiors::InteriorsStage::default()), Stage::Connectivity => Box::new(connectivity::ConnectivityStage::default()), Stage::Dressing => Box::new(dressing::DressingStage::default()), + Stage::Naming => Box::new(naming::NamingStage::default()), _ => Box::new(NoopStage), } } diff --git a/crates/citygen/src/generate/naming.rs b/crates/citygen/src/generate/naming.rs new file mode 100644 index 0000000..613235e --- /dev/null +++ b/crates/citygen/src/generate/naming.rs @@ -0,0 +1,98 @@ +//! Stage 10: names for the city, districts and notable buildings. + +use super::{GenCtx, Stage, StageRunner, StepOut}; +use crate::params::GenError; +use rand_chacha::ChaCha8Rng; + +#[derive(Default)] +pub(crate) struct NamingStage { + next: usize, + rng: Option, +} + +impl StageRunner for NamingStage { + fn begin(&mut self, ctx: &mut GenCtx) -> Result<(), GenError> { + let mut rng = ctx.rng(Stage::Naming); + ctx.city.name = ctx.flavor.city_name(&mut rng); + self.rng = Some(rng); + self.next = 0; + Ok(()) + } + + fn step(&mut self, ctx: &mut GenCtx) -> Result { + let n = ctx.city.districts.len(); + if self.next >= n { + return Ok(StepOut::Finished); + } + let rng = self.rng.as_mut().expect("begin sets rng"); + let d = self.next; + let theme = ctx.city.districts[d].theme; + let district_name = ctx.flavor.district_name(theme, rng); + ctx.city.districts[d].name = district_name.clone(); + + let id = ctx.city.districts[d].id; + let mut corp_names: Vec = Vec::new(); + for i in 0..ctx.city.buildings.len() { + if ctx.city.buildings[i].district != id { + continue; + } + let kind = ctx.city.buildings[i].kind; + let name = if kind.is_corp() { + let corp = ctx.flavor.corp_name(rng); + corp_names.push(corp.clone()); + Some(corp) + } else { + ctx.flavor.shop_name(kind, rng) + }; + ctx.city.buildings[i].name = name; + } + + // Two landmarks per district: the largest footprints get grand names. + let mut candidates: Vec = (0..ctx.city.buildings.len()) + .filter(|i| ctx.city.buildings[*i].district == id) + .collect(); + candidates.sort_by_key(|i| std::cmp::Reverse(ctx.city.buildings[*i].footprint.area())); + for i in candidates.into_iter().take(2) { + let corp = corp_names + .first() + .cloned() + .unwrap_or_else(|| ctx.flavor.corp_name(rng)); + let name = ctx.flavor.landmark_name(&corp, &district_name, rng); + ctx.city.buildings[i].name = Some(name); + } + + self.next += 1; + Ok(StepOut::Continue(self.next as f32 / n as f32)) + } +} + +#[cfg(test)] +mod tests { + use crate::generate::testutil::small; + use crate::params::WaterMode; + + #[test] + fn names_are_filled_and_deterministic() { + let a = small(91, WaterMode::Bay, 7); + let b = small(91, WaterMode::Bay, 7); + assert!(!a.name.is_empty()); + assert_eq!(a.name, b.name); + assert!(a.districts.iter().all(|d| !d.name.is_empty())); + let named = a.buildings.iter().filter(|b| b.name.is_some()).count(); + assert!(named >= a.buildings.iter().filter(|b| b.kind.is_corp()).count()); + let landmarks = a + .buildings + .iter() + .filter(|b| { + b.name.as_deref().is_some_and(|n| { + n.contains("Tower") + || n.contains("Terminal") + || n.contains("Arcology") + || n.contains("Plaza") + || n.contains("Station") + }) + }) + .count(); + assert!(landmarks >= a.districts.len(), "landmarks {landmarks}"); + } +}