//! **Planeamiento urbano**: convierte la mancha de pueblo de un bioma en una traza //! LIMPIA y NIVELADA — plaza central con iglesia, avenidas en cruz, cuadras //! regladas con calles y veredas — **sin escalones**. El suelo del casco se aplana //! a una sola cota (la del centro del pueblo) y el borde se funde con el terreno //! natural por una rampa perimetral, así el auto rueda por calles planas. //! //! Como todo el world-gen, es **función pura de coordenadas de mundo**: cada //! columna `(wx, wz)` se consulta sola, sin estado compartido, así el *streaming* //! encaja ventana a ventana (mismo pueblo en cualquier encuadre). //! //! El retículo: el mundo se parte en celdas de [`CELDA`] voxels; **cada** celda //! aloja un pueblo, centrado con jitter. El `densidad_urbana` del bioma mueve el //! **radio** del casco (aldea chica ↔ ciudad extendida), no la existencia — así la //! traza siempre aparece y es deterministamente ubicable. use crate::terrain::hash2; /// Lado de la celda del retículo de pueblos (voxels). Cada celda aloja un pueblo. /// Grande respecto al radio máximo → queda campo entre pueblos. const CELDA: i32 = 176; /// Radio máximo del casco urbano (ciudad densa), en voxels. const RADIO_MAX: i32 = 62; /// Radio mínimo del casco (aldea dispersa), en voxels. const RADIO_MIN: i32 = 26; /// Jitter máximo del centro dentro de la celda (voxels). const JITTER: i32 = 26; /// Franja perimetral (voxels) donde la cota nivelada se funde con el terreno /// natural: la **rampa** que evita el acantilado en el borde del casco. const RIM: i32 = 12; /// Paso de la grilla de cuadras (voxels): ancho de cuadra + calle. const CUADRA: i32 = 22; /// Media-anchura de calle local (voxels). Ancho total = `2·HW − 1`. const CALLE_HW: i32 = 3; /// Media-anchura de avenida: las dos líneas centrales de la cruz, más anchas. const AVE_HW: i32 = 5; /// Media-lado de la plaza central empedrada (voxels). const PLAZA: i32 = 8; // Paleta de la calzada (se conserva la del pueblo viejo para no romper tests). const ASF_CALLE: [u8; 3] = [64, 66, 72]; // asfalto de calle local const ASF_AVE: [u8; 3] = [58, 60, 66]; // asfalto de avenida const VEREDA: [u8; 3] = [150, 150, 146]; // vereda / cordón const LINEA: [u8; 3] = [200, 178, 60]; // línea amarilla central const PLAZA_A: [u8; 3] = [196, 190, 176]; // baldosa clara const PLAZA_B: [u8; 3] = [150, 150, 146]; // baldosa oscura /// Un pueblo ubicado: su centro de mundo, la cota nivelada del casco y una semilla /// estable propia (para variar estilos entre pueblos). #[derive(Clone, Copy, Debug)] pub struct Pueblo { pub centro: [i32; 2], /// Cota del suelo nivelado (índice `y` del voxel superior del casco). pub suelo: u32, /// Radio del casco urbano (voxels). pub radio: i32, /// Semilla estable del pueblo. pub seed: u32, } /// Zonificación de una columna del casco (lo que se pinta en la superficie). #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Zona { /// Interior de cuadra: suelo natural (patio); puede recibir una casa en el centro. Parcela, /// Plaza empedrada. Plaza, /// Avenida (calzada ancha de la cruz central). Avenida, /// Calle local. Calle, /// Vereda / cordón. Vereda, /// Rampa perimetral: suelo natural en transición, sin pavimento. Rampa, } impl Zona { /// Color a estampar en la superficie, o `None` si conserva el material natural. fn pav(self, u: i32, v: i32) -> Option<[u8; 3]> { match self { Zona::Parcela | Zona::Rampa => None, Zona::Vereda => Some(VEREDA), Zona::Calle => Some(ASF_CALLE), Zona::Avenida => { // Línea amarilla discontinua sobre el eje de la avenida. let sobre_eje = u == 0 || v == 0; let guion = (u + v).rem_euclid(6) < 3; if sobre_eje && guion { Some(LINEA) } else { Some(ASF_AVE) } } Zona::Plaza => Some(if (u + v).rem_euclid(2) == 0 { PLAZA_A } else { PLAZA_B }), } } } /// Resultado de consultar una columna: la cota del suelo (nivelada o rampa) y el /// pavimento a pintar encima (si lo hay). #[derive(Clone, Copy, Debug)] pub struct Plano { pub suelo: u32, pub pav: Option<[u8; 3]>, } /// El **planificador urbano** de un bioma: retículo de pueblos parametrizado por la /// densidad urbana. Sin estado — todo se deriva de `(seed, wx, wz)`. #[derive(Clone, Copy, Debug)] pub struct Urbe { seed: u32, /// Radio del casco, interpolado por `densidad_urbana`. radio: i32, dy: u32, } impl Urbe { /// Construye el planificador para una `seed`, un `densidad_urbana ∈ [0,1]` y el /// alto del mundo `dy`. pub fn new(seed: u32, densidad_urbana: f32, dy: u32) -> Self { let d = densidad_urbana.clamp(0.0, 1.0); let radio = RADIO_MIN + ((RADIO_MAX - RADIO_MIN) as f32 * d) as i32; Urbe { seed, radio, dy } } /// Centro de mundo del pueblo de la celda `(cx, cz)` del retículo (con jitter), /// y su semilla estable. fn centro_celda(&self, cx: i32, cz: i32) -> (i32, i32, u32) { let s = self .seed .wrapping_add((cx as u32).wrapping_mul(0x9E37_79B9)) .wrapping_add((cz as u32).wrapping_mul(0x85EB_CA77)) ^ 0x7011; let jx = ((hash2(cx, cz, s ^ 0xA1) - 0.5) * 2.0 * JITTER as f32) as i32; let jz = ((hash2(cx, cz, s ^ 0xB2) - 0.5) * 2.0 * JITTER as f32) as i32; let bx = cx * CELDA + CELDA / 2 + jx; let bz = cz * CELDA + CELDA / 2 + jz; (bx, bz, s) } /// El pueblo cuyo casco cubre la columna `(wx, wz)`, o `None` si es campo. `h` /// da la altura natural del terreno (para fijar la cota nivelada del casco en el /// centro y para la rampa perimetral). pub fn pueblo_en(&self, wx: i32, wz: i32, h: &impl Fn(i32, i32) -> u32) -> Option<(Pueblo, i32)> { let cx0 = wx.div_euclid(CELDA); let cz0 = wz.div_euclid(CELDA); let mut mejor: Option<(Pueblo, i32)> = None; for dcx in -1..=1 { for dcz in -1..=1 { let (bx, bz, s) = self.centro_celda(cx0 + dcx, cz0 + dcz); let dx = wx - bx; let dz = wz - bz; let d2 = dx * dx + dz * dz; if d2 > self.radio * self.radio { continue; } let d = (d2 as f32).sqrt() as i32; if mejor.map_or(true, |(_, md)| d < md) { let suelo = h(bx, bz).clamp(1, self.dy.saturating_sub(2)); mejor = Some(( Pueblo { centro: [bx, bz], suelo, radio: self.radio, seed: s }, d, )); } } } mejor } /// Consulta la columna `(wx, wz)`: cota del suelo (nivelada dentro del casco, /// rampa hacia el terreno natural en el borde) y pavimento a pintar. `None` = /// campo abierto (el terreno manda). pub fn consultar(&self, wx: i32, wz: i32, h: &impl Fn(i32, i32) -> u32) -> Option { let (p, d) = self.pueblo_en(wx, wz, h)?; let natural = h(wx, wz); let interior = p.radio - RIM; let suelo = if d <= interior { p.suelo } else { // Rampa: mezcla lineal cota↔natural sobre la franja perimetral. let t = ((d - interior) as f32 / RIM as f32).clamp(0.0, 1.0); let a = p.suelo as f32; let b = natural as f32; (a + (b - a) * t).round() as u32 }; // Sólo el interior del casco (dejando un margen antes de la rampa) recibe // traza urbana; la franja perimetral queda como suelo natural en rampa. if d > interior - 2 { return Some(Plano { suelo, pav: Zona::Rampa.pav(0, 0) }); } let (u, v) = (wx - p.centro[0], wz - p.centro[1]); let zona = self.zona(u, v); Some(Plano { suelo, pav: zona.pav(u, v) }) } /// Zonificación de la columna en coordenadas **locales** al centro del pueblo. /// Plaza central → cruz de avenidas → grilla de calles → interior de cuadra. pub fn zona(&self, u: i32, v: i32) -> Zona { if u.abs() <= PLAZA && v.abs() <= PLAZA { return Zona::Plaza; } let bu = banda(u); let bv = banda(v); match (bu, bv) { (None, None) => Zona::Parcela, _ => { // ¿Alguna banda tiene asfalto interior en esta columna? (no sólo cordón) let interior_u = bu.map_or(false, |b| b.d < b.hw - 1); let interior_v = bv.map_or(false, |b| b.d < b.hw - 1); if !interior_u && !interior_v { return Zona::Vereda; } let avenida = bu.map_or(false, |b| b.ave) || bv.map_or(false, |b| b.ave); if avenida { Zona::Avenida } else { Zona::Calle } } } } /// El **radio interior** urbanizable (dentro de la rampa) — lo usan los pases de /// casas / iglesia para no plantar nada en la rampa. pub fn radio_interior(&self) -> i32 { self.radio - RIM - 2 } } /// Una banda de calzada sobre un eje: la grilla centrada tiene líneas en los /// múltiplos de [`CUADRA`]; la línea `k = 0` es avenida (más ancha). #[derive(Clone, Copy)] struct Banda { /// Distancia al eje de la calzada más cercana. d: i32, /// Media-anchura de esa calzada. hw: i32, /// `true` si es una de las dos avenidas centrales. ave: bool, } /// La banda de calzada que cubre la coordenada local `t`, o `None` si cae en cuadra. fn banda(t: i32) -> Option { let k = (t as f32 / CUADRA as f32).round() as i32; let d = (t - k * CUADRA).abs(); let ave = k == 0; let hw = if ave { AVE_HW } else { CALLE_HW }; if d < hw { Some(Banda { d, hw, ave }) } else { None } } /// Los pueblos cuyos cascos intersecan la ventana `origin..origin+dim` (más el /// margen del radio). Para los pases de mobiliario/casas que estampan una vez por /// pueblo (iglesia, fuente, cuadras). pub fn pueblos_en_ventana( urbe: &Urbe, origin: [i32; 2], dim: [u32; 3], h: &impl Fn(i32, i32) -> u32, ) -> Vec { let [ox, oz] = origin; let cx0 = (ox - urbe.radio).div_euclid(CELDA); let cx1 = (ox + dim[0] as i32 + urbe.radio).div_euclid(CELDA); let cz0 = (oz - urbe.radio).div_euclid(CELDA); let cz1 = (oz + dim[2] as i32 + urbe.radio).div_euclid(CELDA); let mut out = Vec::new(); for cx in cx0..=cx1 { for cz in cz0..=cz1 { let (bx, bz, s) = urbe.centro_celda(cx, cz); let suelo = h(bx, bz).clamp(1, urbe.dy.saturating_sub(2)); out.push(Pueblo { centro: [bx, bz], suelo, radio: urbe.radio, seed: s }); } } out } // Offsets locales del mobiliario cívico (relativos al centro del pueblo). /// Desplazamiento en `v` (norte) del centro de la iglesia respecto a la plaza. pub const IGLESIA_V: i32 = -(PLAZA + 6); /// Media-lado de la plaza (para colocar bancos/árboles/iglesia desde afuera). pub const PLAZA_HALF: i32 = PLAZA; /// Paso de la grilla de cuadras, reexportado para el pase de casas. pub const PASO_CUADRA: i32 = CUADRA; #[cfg(test)] mod tests { use super::*; fn llano(_x: i32, _z: i32) -> u32 { 12 } #[test] fn cada_celda_aloja_un_pueblo_ubicable() { let urbe = Urbe::new(7, 0.7, 40); // Consultado en su centro exacto, el pueblo de la celda (0,0) da distancia 0. let (bx, bz, _) = urbe.centro_celda(0, 0); let (p, d) = urbe.pueblo_en(bx, bz, &llano).expect("hay pueblo en la celda 0"); assert_eq!(d, 0); assert_eq!(p.suelo, 12); assert_eq!(p.centro, [bx, bz]); } #[test] fn el_centro_es_plaza() { let urbe = Urbe::new(3, 0.6, 40); assert_eq!(urbe.zona(0, 0), Zona::Plaza); assert_eq!(urbe.zona(PLAZA, 0), Zona::Plaza); // Justo afuera de la plaza, sobre el eje, hay avenida. assert_eq!(urbe.zona(PLAZA + 1, 0), Zona::Avenida); } #[test] fn el_casco_esta_nivelado_sin_escalones() { // Terreno inclinado en rampa: el casco debe salir PLANO (sin escalones). let pend = |x: i32, _z: i32| (10 + x / 3).max(1) as u32; let urbe = Urbe::new(11, 0.8, 60); let (bx, bz, _) = urbe.centro_celda(0, 0); let datum = pend(bx, bz); // Recorremos una franja de avenida (v=0) por el interior del casco: toda a // la misma cota, sin saltos. let mut previa: Option = None; let mut escalones = 0; for du in -urbe.radio_interior()..=urbe.radio_interior() { if let Some(p) = urbe.consultar(bx + du, bz, &pend) { assert_eq!(p.suelo, datum, "el casco no está nivelado en du={du}"); if let Some(pr) = previa { if p.suelo.abs_diff(pr) > 0 { escalones += 1; } } previa = Some(p.suelo); } } assert_eq!(escalones, 0, "la avenida tiene escalones"); } #[test] fn la_avenida_tiene_calzada_y_linea() { let urbe = Urbe::new(5, 0.7, 40); // Sobre el eje de la avenida, más allá de la plaza, hay línea o asfalto. let z = urbe.zona(0, PLAZA + 3); assert_eq!(z, Zona::Avenida); } }