tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! **Planeamiento urbano**: convierte la mancha de pueblo de un bioma en una traza
2//! LIMPIA y NIVELADA — plaza central con iglesia, avenidas en cruz, cuadras
3//! regladas con calles y veredas — **sin escalones**. El suelo del casco se aplana
4//! a una sola cota (la del centro del pueblo) y el borde se funde con el terreno
5//! natural por una rampa perimetral, así el auto rueda por calles planas.
6//!
7//! Como todo el world-gen, es **función pura de coordenadas de mundo**: cada
8//! columna `(wx, wz)` se consulta sola, sin estado compartido, así el *streaming*
9//! encaja ventana a ventana (mismo pueblo en cualquier encuadre).
10//!
11//! El retículo: el mundo se parte en celdas de [`CELDA`] voxels; **cada** celda
12//! aloja un pueblo, centrado con jitter. El `densidad_urbana` del bioma mueve el
13//! **radio** del casco (aldea chica ↔ ciudad extendida), no la existencia — así la
14//! traza siempre aparece y es deterministamente ubicable.
15
16use crate::terrain::hash2;
17
18/// Lado de la celda del retículo de pueblos (voxels). Cada celda aloja un pueblo.
19/// Grande respecto al radio máximo → queda campo entre pueblos.
20const CELDA: i32 = 176;
21/// Radio máximo del casco urbano (ciudad densa), en voxels.
22const RADIO_MAX: i32 = 62;
23/// Radio mínimo del casco (aldea dispersa), en voxels.
24const RADIO_MIN: i32 = 26;
25/// Jitter máximo del centro dentro de la celda (voxels).
26const JITTER: i32 = 26;
27/// Franja perimetral (voxels) donde la cota nivelada se funde con el terreno
28/// natural: la **rampa** que evita el acantilado en el borde del casco.
29const RIM: i32 = 12;
30/// Paso de la grilla de cuadras (voxels): ancho de cuadra + calle.
31const CUADRA: i32 = 22;
32/// Media-anchura de calle local (voxels). Ancho total = `2·HW − 1`.
33const CALLE_HW: i32 = 3;
34/// Media-anchura de avenida: las dos líneas centrales de la cruz, más anchas.
35const AVE_HW: i32 = 5;
36/// Media-lado de la plaza central empedrada (voxels).
37const PLAZA: i32 = 8;
38
39// Paleta de la calzada (se conserva la del pueblo viejo para no romper tests).
40const ASF_CALLE: [u8; 3] = [64, 66, 72]; // asfalto de calle local
41const ASF_AVE: [u8; 3] = [58, 60, 66]; // asfalto de avenida
42const VEREDA: [u8; 3] = [150, 150, 146]; // vereda / cordón
43const LINEA: [u8; 3] = [200, 178, 60]; // línea amarilla central
44const PLAZA_A: [u8; 3] = [196, 190, 176]; // baldosa clara
45const PLAZA_B: [u8; 3] = [150, 150, 146]; // baldosa oscura
46
47/// Un pueblo ubicado: su centro de mundo, la cota nivelada del casco y una semilla
48/// estable propia (para variar estilos entre pueblos).
49#[derive(Clone, Copy, Debug)]
50pub struct Pueblo {
51 pub centro: [i32; 2],
52 /// Cota del suelo nivelado (índice `y` del voxel superior del casco).
53 pub suelo: u32,
54 /// Radio del casco urbano (voxels).
55 pub radio: i32,
56 /// Semilla estable del pueblo.
57 pub seed: u32,
58}
59
60/// Zonificación de una columna del casco (lo que se pinta en la superficie).
61#[derive(Clone, Copy, PartialEq, Eq, Debug)]
62pub enum Zona {
63 /// Interior de cuadra: suelo natural (patio); puede recibir una casa en el centro.
64 Parcela,
65 /// Plaza empedrada.
66 Plaza,
67 /// Avenida (calzada ancha de la cruz central).
68 Avenida,
69 /// Calle local.
70 Calle,
71 /// Vereda / cordón.
72 Vereda,
73 /// Rampa perimetral: suelo natural en transición, sin pavimento.
74 Rampa,
75}
76
77impl Zona {
78 /// Color a estampar en la superficie, o `None` si conserva el material natural.
79 fn pav(self, u: i32, v: i32) -> Option<[u8; 3]> {
80 match self {
81 Zona::Parcela | Zona::Rampa => None,
82 Zona::Vereda => Some(VEREDA),
83 Zona::Calle => Some(ASF_CALLE),
84 Zona::Avenida => {
85 // Línea amarilla discontinua sobre el eje de la avenida.
86 let sobre_eje = u == 0 || v == 0;
87 let guion = (u + v).rem_euclid(6) < 3;
88 if sobre_eje && guion {
89 Some(LINEA)
90 } else {
91 Some(ASF_AVE)
92 }
93 }
94 Zona::Plaza => Some(if (u + v).rem_euclid(2) == 0 { PLAZA_A } else { PLAZA_B }),
95 }
96 }
97}
98
99/// Resultado de consultar una columna: la cota del suelo (nivelada o rampa) y el
100/// pavimento a pintar encima (si lo hay).
101#[derive(Clone, Copy, Debug)]
102pub struct Plano {
103 pub suelo: u32,
104 pub pav: Option<[u8; 3]>,
105}
106
107/// El **planificador urbano** de un bioma: retículo de pueblos parametrizado por la
108/// densidad urbana. Sin estado — todo se deriva de `(seed, wx, wz)`.
109#[derive(Clone, Copy, Debug)]
110pub struct Urbe {
111 seed: u32,
112 /// Radio del casco, interpolado por `densidad_urbana`.
113 radio: i32,
114 dy: u32,
115}
116
117impl Urbe {
118 /// Construye el planificador para una `seed`, un `densidad_urbana ∈ [0,1]` y el
119 /// alto del mundo `dy`.
120 pub fn new(seed: u32, densidad_urbana: f32, dy: u32) -> Self {
121 let d = densidad_urbana.clamp(0.0, 1.0);
122 let radio = RADIO_MIN + ((RADIO_MAX - RADIO_MIN) as f32 * d) as i32;
123 Urbe { seed, radio, dy }
124 }
125
126 /// Centro de mundo del pueblo de la celda `(cx, cz)` del retículo (con jitter),
127 /// y su semilla estable.
128 fn centro_celda(&self, cx: i32, cz: i32) -> (i32, i32, u32) {
129 let s = self
130 .seed
131 .wrapping_add((cx as u32).wrapping_mul(0x9E37_79B9))
132 .wrapping_add((cz as u32).wrapping_mul(0x85EB_CA77))
133 ^ 0x7011;
134 let jx = ((hash2(cx, cz, s ^ 0xA1) - 0.5) * 2.0 * JITTER as f32) as i32;
135 let jz = ((hash2(cx, cz, s ^ 0xB2) - 0.5) * 2.0 * JITTER as f32) as i32;
136 let bx = cx * CELDA + CELDA / 2 + jx;
137 let bz = cz * CELDA + CELDA / 2 + jz;
138 (bx, bz, s)
139 }
140
141 /// El pueblo cuyo casco cubre la columna `(wx, wz)`, o `None` si es campo. `h`
142 /// da la altura natural del terreno (para fijar la cota nivelada del casco en el
143 /// centro y para la rampa perimetral).
144 pub fn pueblo_en(&self, wx: i32, wz: i32, h: &impl Fn(i32, i32) -> u32) -> Option<(Pueblo, i32)> {
145 let cx0 = wx.div_euclid(CELDA);
146 let cz0 = wz.div_euclid(CELDA);
147 let mut mejor: Option<(Pueblo, i32)> = None;
148 for dcx in -1..=1 {
149 for dcz in -1..=1 {
150 let (bx, bz, s) = self.centro_celda(cx0 + dcx, cz0 + dcz);
151 let dx = wx - bx;
152 let dz = wz - bz;
153 let d2 = dx * dx + dz * dz;
154 if d2 > self.radio * self.radio {
155 continue;
156 }
157 let d = (d2 as f32).sqrt() as i32;
158 if mejor.map_or(true, |(_, md)| d < md) {
159 let suelo = h(bx, bz).clamp(1, self.dy.saturating_sub(2));
160 mejor = Some((
161 Pueblo { centro: [bx, bz], suelo, radio: self.radio, seed: s },
162 d,
163 ));
164 }
165 }
166 }
167 mejor
168 }
169
170 /// Consulta la columna `(wx, wz)`: cota del suelo (nivelada dentro del casco,
171 /// rampa hacia el terreno natural en el borde) y pavimento a pintar. `None` =
172 /// campo abierto (el terreno manda).
173 pub fn consultar(&self, wx: i32, wz: i32, h: &impl Fn(i32, i32) -> u32) -> Option<Plano> {
174 let (p, d) = self.pueblo_en(wx, wz, h)?;
175 let natural = h(wx, wz);
176 let interior = p.radio - RIM;
177 let suelo = if d <= interior {
178 p.suelo
179 } else {
180 // Rampa: mezcla lineal cota↔natural sobre la franja perimetral.
181 let t = ((d - interior) as f32 / RIM as f32).clamp(0.0, 1.0);
182 let a = p.suelo as f32;
183 let b = natural as f32;
184 (a + (b - a) * t).round() as u32
185 };
186 // Sólo el interior del casco (dejando un margen antes de la rampa) recibe
187 // traza urbana; la franja perimetral queda como suelo natural en rampa.
188 if d > interior - 2 {
189 return Some(Plano { suelo, pav: Zona::Rampa.pav(0, 0) });
190 }
191 let (u, v) = (wx - p.centro[0], wz - p.centro[1]);
192 let zona = self.zona(u, v);
193 Some(Plano { suelo, pav: zona.pav(u, v) })
194 }
195
196 /// Zonificación de la columna en coordenadas **locales** al centro del pueblo.
197 /// Plaza central → cruz de avenidas → grilla de calles → interior de cuadra.
198 pub fn zona(&self, u: i32, v: i32) -> Zona {
199 if u.abs() <= PLAZA && v.abs() <= PLAZA {
200 return Zona::Plaza;
201 }
202 let bu = banda(u);
203 let bv = banda(v);
204 match (bu, bv) {
205 (None, None) => Zona::Parcela,
206 _ => {
207 // ¿Alguna banda tiene asfalto interior en esta columna? (no sólo cordón)
208 let interior_u = bu.map_or(false, |b| b.d < b.hw - 1);
209 let interior_v = bv.map_or(false, |b| b.d < b.hw - 1);
210 if !interior_u && !interior_v {
211 return Zona::Vereda;
212 }
213 let avenida = bu.map_or(false, |b| b.ave) || bv.map_or(false, |b| b.ave);
214 if avenida {
215 Zona::Avenida
216 } else {
217 Zona::Calle
218 }
219 }
220 }
221 }
222
223 /// El **radio interior** urbanizable (dentro de la rampa) — lo usan los pases de
224 /// casas / iglesia para no plantar nada en la rampa.
225 pub fn radio_interior(&self) -> i32 {
226 self.radio - RIM - 2
227 }
228}
229
230/// Una banda de calzada sobre un eje: la grilla centrada tiene líneas en los
231/// múltiplos de [`CUADRA`]; la línea `k = 0` es avenida (más ancha).
232#[derive(Clone, Copy)]
233struct Banda {
234 /// Distancia al eje de la calzada más cercana.
235 d: i32,
236 /// Media-anchura de esa calzada.
237 hw: i32,
238 /// `true` si es una de las dos avenidas centrales.
239 ave: bool,
240}
241
242/// La banda de calzada que cubre la coordenada local `t`, o `None` si cae en cuadra.
243fn banda(t: i32) -> Option<Banda> {
244 let k = (t as f32 / CUADRA as f32).round() as i32;
245 let d = (t - k * CUADRA).abs();
246 let ave = k == 0;
247 let hw = if ave { AVE_HW } else { CALLE_HW };
248 if d < hw {
249 Some(Banda { d, hw, ave })
250 } else {
251 None
252 }
253}
254
255/// Los pueblos cuyos cascos intersecan la ventana `origin..origin+dim` (más el
256/// margen del radio). Para los pases de mobiliario/casas que estampan una vez por
257/// pueblo (iglesia, fuente, cuadras).
258pub fn pueblos_en_ventana(
259 urbe: &Urbe,
260 origin: [i32; 2],
261 dim: [u32; 3],
262 h: &impl Fn(i32, i32) -> u32,
263) -> Vec<Pueblo> {
264 let [ox, oz] = origin;
265 let cx0 = (ox - urbe.radio).div_euclid(CELDA);
266 let cx1 = (ox + dim[0] as i32 + urbe.radio).div_euclid(CELDA);
267 let cz0 = (oz - urbe.radio).div_euclid(CELDA);
268 let cz1 = (oz + dim[2] as i32 + urbe.radio).div_euclid(CELDA);
269 let mut out = Vec::new();
270 for cx in cx0..=cx1 {
271 for cz in cz0..=cz1 {
272 let (bx, bz, s) = urbe.centro_celda(cx, cz);
273 let suelo = h(bx, bz).clamp(1, urbe.dy.saturating_sub(2));
274 out.push(Pueblo { centro: [bx, bz], suelo, radio: urbe.radio, seed: s });
275 }
276 }
277 out
278}
279
280// Offsets locales del mobiliario cívico (relativos al centro del pueblo).
281/// Desplazamiento en `v` (norte) del centro de la iglesia respecto a la plaza.
282pub const IGLESIA_V: i32 = -(PLAZA + 6);
283/// Media-lado de la plaza (para colocar bancos/árboles/iglesia desde afuera).
284pub const PLAZA_HALF: i32 = PLAZA;
285/// Paso de la grilla de cuadras, reexportado para el pase de casas.
286pub const PASO_CUADRA: i32 = CUADRA;
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn llano(_x: i32, _z: i32) -> u32 {
293 12
294 }
295
296 #[test]
297 fn cada_celda_aloja_un_pueblo_ubicable() {
298 let urbe = Urbe::new(7, 0.7, 40);
299 // Consultado en su centro exacto, el pueblo de la celda (0,0) da distancia 0.
300 let (bx, bz, _) = urbe.centro_celda(0, 0);
301 let (p, d) = urbe.pueblo_en(bx, bz, &llano).expect("hay pueblo en la celda 0");
302 assert_eq!(d, 0);
303 assert_eq!(p.suelo, 12);
304 assert_eq!(p.centro, [bx, bz]);
305 }
306
307 #[test]
308 fn el_centro_es_plaza() {
309 let urbe = Urbe::new(3, 0.6, 40);
310 assert_eq!(urbe.zona(0, 0), Zona::Plaza);
311 assert_eq!(urbe.zona(PLAZA, 0), Zona::Plaza);
312 // Justo afuera de la plaza, sobre el eje, hay avenida.
313 assert_eq!(urbe.zona(PLAZA + 1, 0), Zona::Avenida);
314 }
315
316 #[test]
317 fn el_casco_esta_nivelado_sin_escalones() {
318 // Terreno inclinado en rampa: el casco debe salir PLANO (sin escalones).
319 let pend = |x: i32, _z: i32| (10 + x / 3).max(1) as u32;
320 let urbe = Urbe::new(11, 0.8, 60);
321 let (bx, bz, _) = urbe.centro_celda(0, 0);
322 let datum = pend(bx, bz);
323 // Recorremos una franja de avenida (v=0) por el interior del casco: toda a
324 // la misma cota, sin saltos.
325 let mut previa: Option<u32> = None;
326 let mut escalones = 0;
327 for du in -urbe.radio_interior()..=urbe.radio_interior() {
328 if let Some(p) = urbe.consultar(bx + du, bz, &pend) {
329 assert_eq!(p.suelo, datum, "el casco no está nivelado en du={du}");
330 if let Some(pr) = previa {
331 if p.suelo.abs_diff(pr) > 0 {
332 escalones += 1;
333 }
334 }
335 previa = Some(p.suelo);
336 }
337 }
338 assert_eq!(escalones, 0, "la avenida tiene escalones");
339 }
340
341 #[test]
342 fn la_avenida_tiene_calzada_y_linea() {
343 let urbe = Urbe::new(5, 0.7, 40);
344 // Sobre el eje de la avenida, más allá de la plaza, hay línea o asfalto.
345 let z = urbe.zona(0, PLAZA + 3);
346 assert_eq!(z, Zona::Avenida);
347 }
348}