tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1//! **Casas procedurales por tipo**: a diferencia de un [`Mueble`](crate::Mueble)
2//! (modelo fijo), una casa se **genera** — dimensiones, ventanas, techo, chimenea y
3//! detalles varían con una **semilla**, dentro de un **estilo** ([`CasaTipo`]).
4//! Estampá una con [`stamp_casa`]; misma `(tipo, seed)` → misma casa (determinista),
5//! semillas distintas → casas distintas del mismo estilo.
6
7use serde::{Deserialize, Serialize};
8
9use crate::mineral::Mineral;
10use crate::worldgen::Material;
11use llimphi_3d::VoxelGrid;
12
13/// Estilo de casa. Cada uno tiene su gramática de generación.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub enum CasaTipo {
16 /// Prehistórica (Flintstones): choza de piedra abovedada, puerta en arco.
17 Flintstone,
18 /// Hobbit: montículo de pasto con puerta y ventana redondas.
19 Hobbit,
20 /// Cabaña de troncos: muros de madera, techo a dos aguas, chimenea de piedra.
21 Cabana,
22 /// Casa moderna: hormigón y grandes ventanales de vidrio, techo plano.
23 Moderna,
24 /// Edificio de departamentos: bloque alto con grilla de ventanas.
25 Edificio,
26 /// Iglú: cúpula de nieve con túnel de entrada.
27 Iglu,
28 /// Palafito: casa de madera sobre pilotes.
29 Palafito,
30 /// Adobe (pueblo): bloques de adobe apilados, techos planos y vigas.
31 Adobe,
32 /// Rascacielos: torre de vidrio y acero (altura variable).
33 Rascacielos,
34 /// Granja: casa con techo a dos aguas + granero.
35 Granja,
36 /// Torre medieval: torreón de piedra con almenas.
37 Torre,
38 /// Faro costero: torre blanca con franjas rojas y linterna que emite luz.
39 Faro,
40 /// Choza tropical: paredes bajas y techo cónico de paja.
41 Choza,
42 /// Tienda: local a pie de calle con toldo y vidriera.
43 Tienda,
44 /// Dúplex suburbano: dos plantas, techo a dos aguas, garaje.
45 Duplex,
46 /// Palacio de cuento: mármol sobre una escalinata, cuatro torres con
47 /// agujas de diamante y una cúpula de diamante con remate de oro — «un
48 /// lindo palacio de diamantes». Va al final: postcard numera las variantes.
49 Palacio,
50 /// Kiosko de jardín abierto: ocho columnas y un techo en domo, de
51 /// malaquita (franjas de cobre y jade) con borde y remate de oro.
52 Kiosko,
53}
54
55impl CasaTipo {
56 pub const ALL: [CasaTipo; 17] = [
57 CasaTipo::Flintstone,
58 CasaTipo::Hobbit,
59 CasaTipo::Cabana,
60 CasaTipo::Moderna,
61 CasaTipo::Edificio,
62 CasaTipo::Iglu,
63 CasaTipo::Palafito,
64 CasaTipo::Adobe,
65 CasaTipo::Rascacielos,
66 CasaTipo::Granja,
67 CasaTipo::Torre,
68 CasaTipo::Faro,
69 CasaTipo::Choza,
70 CasaTipo::Tienda,
71 CasaTipo::Duplex,
72 CasaTipo::Palacio,
73 CasaTipo::Kiosko,
74 ];
75
76 pub fn label(self) -> &'static str {
77 match self {
78 CasaTipo::Flintstone => "flintstone",
79 CasaTipo::Hobbit => "hobbit",
80 CasaTipo::Cabana => "cabaña",
81 CasaTipo::Moderna => "moderna",
82 CasaTipo::Edificio => "edificio",
83 CasaTipo::Iglu => "iglú",
84 CasaTipo::Palafito => "palafito",
85 CasaTipo::Adobe => "adobe",
86 CasaTipo::Rascacielos => "rascacielos",
87 CasaTipo::Granja => "granja",
88 CasaTipo::Torre => "torre",
89 CasaTipo::Faro => "faro",
90 CasaTipo::Choza => "choza",
91 CasaTipo::Tienda => "tienda",
92 CasaTipo::Duplex => "dúplex",
93 CasaTipo::Palacio => "palacio",
94 CasaTipo::Kiosko => "kiosko",
95 }
96 }
97
98 /// Radio de la **huella** en planta (voxels): hasta dónde llega la casa desde
99 /// su centro. Es lo que hay que nivelar y despejar para plantarla a mano
100 /// ([`plantar_casa`]). Generoso: mejor despejar de más que dejar un árbol
101 /// atravesando un muro.
102 pub fn huella(self) -> i32 {
103 match self {
104 // La esquina de la escalinata está a (w+3, d+4) = (19, 16): 25 cubre
105 // el palacio más grande y deja un voxel de aire alrededor.
106 CasaTipo::Palacio => 25,
107 CasaTipo::Kiosko => 9,
108 CasaTipo::Rascacielos | CasaTipo::Granja | CasaTipo::Duplex => 12,
109 _ => 9,
110 }
111 }
112
113 pub fn next(self) -> CasaTipo {
114 let i = CasaTipo::ALL.iter().position(|&t| t == self).unwrap_or(0);
115 CasaTipo::ALL[(i + 1) % CasaTipo::ALL.len()]
116 }
117}
118
119/// **Uso de casas en un bioma**: qué estilo y con qué densidad `[0,1]` aparecen
120/// (una candidata por parcela de la grilla gruesa; el bioma las esparce por semilla
121/// → un pueblo/ciudad procedural, como la flora).
122#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
123pub struct CasaUso {
124 pub tipo: CasaTipo,
125 pub densidad: f32,
126}
127
128/// **Tipo de asentamiento**: preset que rellena la mezcla de casas ([`CasaUso`])
129/// y la densidad urbana de un bioma → distintos pueblos y ciudades de un toque.
130/// Es sólo un atajo de autoría: al aplicarlo, escribe `casas` + `densidad_urbana`.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
132pub enum PuebloTipo {
133 /// Aldea: puñado de casas rústicas, muy dispersa.
134 Aldea,
135 /// Suburbio: dúplex y casas modernas en densidad media.
136 Suburbio,
137 /// Ciudad: mezcla de edificios y casas, densa.
138 Ciudad,
139 /// Metrópolis: rascacielos y torres de departamentos, muy densa.
140 Metropolis,
141 /// Prehistórico: chozas, flintstones y hobbits, dispersa.
142 Prehistorico,
143 /// Costero: faros, palafitos y chozas junto al agua.
144 Costero,
145 /// Medieval: torres, adobe y cabañas amuralladas.
146 Medieval,
147 /// Rural: granjas y cabañas muy espaciadas.
148 Rural,
149}
150
151impl PuebloTipo {
152 pub const ALL: [PuebloTipo; 8] = [
153 PuebloTipo::Aldea,
154 PuebloTipo::Suburbio,
155 PuebloTipo::Ciudad,
156 PuebloTipo::Metropolis,
157 PuebloTipo::Prehistorico,
158 PuebloTipo::Costero,
159 PuebloTipo::Medieval,
160 PuebloTipo::Rural,
161 ];
162
163 pub fn label(self) -> &'static str {
164 match self {
165 PuebloTipo::Aldea => "aldea",
166 PuebloTipo::Suburbio => "suburbio",
167 PuebloTipo::Ciudad => "ciudad",
168 PuebloTipo::Metropolis => "metrópolis",
169 PuebloTipo::Prehistorico => "prehistórico",
170 PuebloTipo::Costero => "costero",
171 PuebloTipo::Medieval => "medieval",
172 PuebloTipo::Rural => "rural",
173 }
174 }
175
176 pub fn next(self) -> PuebloTipo {
177 let i = PuebloTipo::ALL.iter().position(|&t| t == self).unwrap_or(0);
178 PuebloTipo::ALL[(i + 1) % PuebloTipo::ALL.len()]
179 }
180
181 /// Densidad urbana sugerida `[0,1]`: cuánto se agrupan las casas (cluster fbm).
182 pub fn densidad(self) -> f32 {
183 match self {
184 PuebloTipo::Aldea => 0.25,
185 PuebloTipo::Suburbio => 0.5,
186 PuebloTipo::Ciudad => 0.7,
187 PuebloTipo::Metropolis => 0.9,
188 PuebloTipo::Prehistorico => 0.3,
189 PuebloTipo::Costero => 0.4,
190 PuebloTipo::Medieval => 0.55,
191 PuebloTipo::Rural => 0.15,
192 }
193 }
194
195 /// Mezcla de casas (estilo + probabilidad por parcela) característica del tipo.
196 pub fn casas(self) -> Vec<CasaUso> {
197 let c = |tipo, densidad| CasaUso { tipo, densidad };
198 match self {
199 PuebloTipo::Aldea => vec![
200 c(CasaTipo::Cabana, 0.4),
201 c(CasaTipo::Adobe, 0.3),
202 c(CasaTipo::Granja, 0.2),
203 c(CasaTipo::Choza, 0.2),
204 ],
205 PuebloTipo::Suburbio => vec![
206 c(CasaTipo::Duplex, 0.45),
207 c(CasaTipo::Moderna, 0.4),
208 c(CasaTipo::Tienda, 0.15),
209 c(CasaTipo::Granja, 0.1),
210 ],
211 PuebloTipo::Ciudad => vec![
212 c(CasaTipo::Edificio, 0.45),
213 c(CasaTipo::Moderna, 0.3),
214 c(CasaTipo::Tienda, 0.25),
215 c(CasaTipo::Duplex, 0.2),
216 ],
217 PuebloTipo::Metropolis => vec![
218 c(CasaTipo::Rascacielos, 0.4),
219 c(CasaTipo::Edificio, 0.5),
220 c(CasaTipo::Moderna, 0.2),
221 c(CasaTipo::Tienda, 0.2),
222 ],
223 PuebloTipo::Prehistorico => vec![
224 c(CasaTipo::Flintstone, 0.4),
225 c(CasaTipo::Choza, 0.3),
226 c(CasaTipo::Hobbit, 0.25),
227 ],
228 PuebloTipo::Costero => vec![
229 c(CasaTipo::Palafito, 0.4),
230 c(CasaTipo::Choza, 0.3),
231 c(CasaTipo::Cabana, 0.2),
232 c(CasaTipo::Faro, 0.06),
233 ],
234 PuebloTipo::Medieval => vec![
235 c(CasaTipo::Adobe, 0.4),
236 c(CasaTipo::Cabana, 0.3),
237 c(CasaTipo::Torre, 0.15),
238 c(CasaTipo::Granja, 0.2),
239 ],
240 PuebloTipo::Rural => vec![
241 c(CasaTipo::Granja, 0.4),
242 c(CasaTipo::Cabana, 0.3),
243 c(CasaTipo::Choza, 0.15),
244 ],
245 }
246 }
247}
248
249/// Deriva una semilla de casa estable de una parcela de mundo `(cwx, cwz)` y la
250/// semilla del mundo → cada parcela genera una casa distinta pero determinista.
251pub fn casa_seed(world_seed: u32, cwx: i32, cwz: i32) -> u32 {
252 world_seed
253 .wrapping_add((cwx as u32).wrapping_mul(0x9E37_79B9))
254 .wrapping_add((cwz as u32).wrapping_mul(0x85EB_CA77))
255 ^ 0xCA5A
256}
257
258/// Hash entero determinista `[0, m)` a partir de `(seed, salt)`.
259#[inline]
260fn h(seed: u32, salt: u32, m: u32) -> u32 {
261 let mut x = seed.wrapping_mul(0x9E37_79B9).wrapping_add(salt.wrapping_mul(0x85EB_CA77));
262 x ^= x >> 15;
263 x = x.wrapping_mul(0x2C1B_3C6D);
264 x ^= x >> 13;
265 x % m.max(1)
266}
267
268/// Ruido `[0,1)` determinista para jitter fino.
269#[inline]
270fn hf(seed: u32, a: i32, b: i32) -> f32 {
271 let mut x = seed
272 .wrapping_add((a as u32).wrapping_mul(0x9E37_79B9))
273 .wrapping_add((b as u32).wrapping_mul(0x85EB_CA77));
274 x ^= x >> 15;
275 x = x.wrapping_mul(0x2C1B_3C6D);
276 x ^= x >> 12;
277 (x & 0xFFFF) as f32 / 65536.0
278}
279
280/// **Genera y estampa una casa** del `tipo` con la base (esquina/centro) en
281/// `(x, y, z)`, variada por `seed`. El piso arranca en `y`.
282pub fn stamp_casa(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, y: u32, z: u32, seed: u32) {
283 let bx = x as i32;
284 let by = y as i32;
285 let bz = z as i32;
286 let dim = g.dim();
287 // **Simétrica por construcción**: cada voxel va también a su espejo en `x`
288 // (el frente mira a `+Z`, así que izquierda y derecha son `±dx`). Lo que un
289 // estilo pone de un solo lado —la chimenea, el garaje, el tanque— sale de
290 // los dos, que es como construye la gente; y lo que varía al azar por
291 // columna queda igual a los dos lados porque el último que escribe escribe
292 // en ambos. Ver `toda_casa_es_simetrica_izquierda_derecha`.
293 let mut put = |dx: i32, dy: i32, dz: i32, mat: Material| {
294 for dx in [dx, -dx] {
295 let (px, py, pz) = (bx + dx, by + dy, bz + dz);
296 if px >= 0 && py >= 0 && pz >= 0 && (px as u32) < dim[0] && (py as u32) < dim[1] && (pz as u32) < dim[2] {
297 g.set_rgba(px as u32, py as u32, pz as u32, mat.color(), VoxelGrid::shine_to_alpha(mat.brillo()));
298 }
299 }
300 };
301
302 match tipo {
303 CasaTipo::Flintstone => {
304 // Choza de piedra abovedada, con silueta lumpy y arco de entrada.
305 let w = 4 + h(seed, 1, 3) as i32; // 4..6 (medio ancho)
306 let d = 4 + h(seed, 2, 3) as i32;
307 let base_h = 4 + h(seed, 3, 2) as i32;
308 let piedra = [Material::Arenisca, Material::Grava, Material::Rock][h(seed, 9, 3) as usize];
309 for dx in -w..=w {
310 for dz in -d..=d {
311 let borde = dx == -w || dx == w || dz == -d || dz == d;
312 if borde {
313 // Altura de muro con bulto (piedra irregular).
314 let bulto = (hf(seed, dx, dz) * 2.5) as i32;
315 let alto = base_h + bulto;
316 for dy in 0..alto {
317 // Arco de puerta en el frente (dz = d).
318 let puerta = dz == d && dx.abs() <= 1 && dy < 3;
319 if !puerta {
320 put(dx, dy, dz, piedra);
321 }
322 }
323 }
324 }
325 }
326 // Techo abovedado (cúpula chata de piedra/paja).
327 let techo = if h(seed, 4, 2) == 0 { Material::Paja } else { piedra };
328 for dx in -w..=w {
329 for dz in -d..=d {
330 let r = (dx * dx) as f32 / (w * w) as f32 + (dz * dz) as f32 / (d * d) as f32;
331 let cima = ((1.0 - r) * 3.0) as i32;
332 if cima >= 0 {
333 put(dx, base_h + cima, dz, techo);
334 }
335 }
336 }
337 }
338
339 CasaTipo::Hobbit => {
340 // Montículo de pasto con puerta y ventana redondas.
341 let r = 5 + h(seed, 1, 3) as i32; // radio 5..7
342 let past = [Material::Grass, Material::Musgo, Material::Leaf][h(seed, 8, 3) as usize];
343 for dx in -r..=r {
344 for dy in 0..=r {
345 for dz in -r..=r {
346 let dd = dx * dx + dy * dy + dz * dz;
347 // Cáscara de la media esfera (montículo).
348 if dd <= r * r && dd > (r - 2) * (r - 2) {
349 put(dx, dy, dz, past);
350 }
351 }
352 }
353 }
354 // Fachada + puerta redonda al frente (dz = r-1..r).
355 let zf = r;
356 for dx in -2..=2 {
357 for dy in 0..4 {
358 if dx * dx + (dy - 1) * (dy - 1) <= 4 {
359 put(dx, dy, zf - 1, Material::Ladrillo); // marco de la puerta
360 }
361 }
362 }
363 for dx in -1..=1 {
364 for dy in 0..3 {
365 put(dx, dy, zf, Material::Madera); // puerta redonda de madera
366 }
367 }
368 put(3, 2, zf - 1, Material::Vidrio); // ventanita redonda
369 put(-3, 2, zf - 1, Material::Vidrio);
370 // Chimenea.
371 put(0, r + 1, 0, Material::Ladrillo);
372 put(0, r + 2, 0, Material::Ladrillo);
373 }
374
375 CasaTipo::Cabana => {
376 // Cabaña de troncos con techo a dos aguas y chimenea de piedra.
377 let w = 3 + h(seed, 1, 3) as i32; // 3..5
378 let d = 4 + h(seed, 2, 3) as i32; // 4..6
379 let wall = 5;
380 for dy in 0..wall {
381 for dx in -w..=w {
382 for dz in -d..=d {
383 let borde = dx == -w || dx == w || dz == -d || dz == d;
384 if borde {
385 // Puerta al frente + ventanas.
386 let puerta = dz == d && dx.abs() <= 1 && dy < 3;
387 let ventana = (dx == -w || dx == w) && dy == 2 && dz.abs() == d / 2;
388 let m = if puerta {
389 continue;
390 } else if ventana {
391 Material::Vidrio
392 } else if dy % 2 == 0 {
393 Material::Madera // troncos (franjas)
394 } else {
395 Material::Adobe
396 };
397 put(dx, dy, dz, m);
398 }
399 }
400 }
401 }
402 // Techo a dos aguas (cumbrera a lo largo del eje más largo).
403 let teja = if h(seed, 4, 2) == 0 { Material::Teja } else { Material::Pizarra };
404 let ridge_x = w >= d;
405 for dx in -w - 1..=w + 1 {
406 for dz in -d - 1..=d + 1 {
407 let slope = if ridge_x { d + 1 - dz.abs() } else { w + 1 - dx.abs() }.max(0);
408 put(dx, wall + slope, dz, teja);
409 }
410 }
411 // Chimenea de piedra a un lado (varía).
412 let cs = if h(seed, 5, 2) == 0 { -w } else { w };
413 for dy in 0..wall + d + 2 {
414 put(cs, dy, -d + 1, Material::Rock);
415 }
416 }
417
418 CasaTipo::Moderna => {
419 // Casa moderna: hormigón + grandes ventanales, techo plano, 1–2 plantas.
420 let w = 4 + h(seed, 1, 3) as i32;
421 let d = 4 + h(seed, 2, 3) as i32;
422 let plantas = 1 + h(seed, 3, 2) as i32; // 1..2
423 let ph = 5; // alto de planta
424 let top = plantas * ph;
425 for dy in 0..top {
426 for dx in -w..=w {
427 for dz in -d..=d {
428 let borde = dx == -w || dx == w || dz == -d || dz == d;
429 if borde {
430 let piso_line = dy % ph == 0;
431 // Ventanal: franjas de vidrio entre líneas de piso.
432 let ventanal = !piso_line && (dy % ph) != ph - 1;
433 let puerta = dz == d && dx.abs() <= 1 && dy < 3;
434 if puerta {
435 continue;
436 }
437 let m = if piso_line {
438 Material::Hormigon
439 } else if ventanal {
440 Material::Vidrio
441 } else {
442 Material::Yeso
443 };
444 put(dx, dy, dz, m);
445 }
446 }
447 }
448 }
449 // Losa/techo plano.
450 for dx in -w..=w {
451 for dz in -d..=d {
452 put(dx, top, dz, Material::Hormigon);
453 }
454 }
455 // Parapeto + a veces un panel solar.
456 if h(seed, 6, 2) == 0 {
457 put(0, top + 1, 0, Material::Azul);
458 put(1, top + 1, 0, Material::Azul);
459 }
460 }
461
462 CasaTipo::Edificio => {
463 // Bloque de departamentos: alto, con grilla de ventanas.
464 let w = 3 + h(seed, 1, 3) as i32;
465 let d = 3 + h(seed, 2, 2) as i32;
466 let pisos = 5 + h(seed, 3, 5) as i32; // 5..9
467 let ph = 3;
468 let top = pisos * ph;
469 let muro = [Material::Ladrillo, Material::Hormigon, Material::Arcilla][h(seed, 7, 3) as usize];
470 for dy in 0..top {
471 for dx in -w..=w {
472 for dz in -d..=d {
473 let borde = dx == -w || dx == w || dz == -d || dz == d;
474 if borde {
475 // Ventana en cada piso (mitad de la altura), columnas alternas.
476 let ventana = dy > 0 && dy % ph == 1 && (dx + dz).rem_euclid(2) == 0;
477 let puerta = dz == d && dx.abs() <= 1 && dy < 2;
478 if puerta {
479 continue;
480 }
481 put(dx, dy, dz, if ventana { Material::Vidrio } else { muro });
482 }
483 }
484 }
485 }
486 for dx in -w..=w {
487 for dz in -d..=d {
488 put(dx, top, dz, Material::Hormigon); // azotea
489 }
490 }
491 // Tanque/antena.
492 put(-w + 1, top + 1, -d + 1, Material::Acero);
493 }
494
495 CasaTipo::Iglu => {
496 let r = 4 + h(seed, 1, 2) as i32; // 4..5
497 for dx in -r..=r {
498 for dy in 0..=r {
499 for dz in -r..=r {
500 let dd = dx * dx + dy * dy + dz * dz;
501 if dd <= r * r && dd > (r - 2) * (r - 2) {
502 put(dx, dy, dz, Material::Snow); // cúpula de nieve
503 }
504 }
505 }
506 }
507 for dz in r..=r + 2 {
508 for dx in -1..=1 {
509 for dy in 0..3 {
510 if dx * dx + (dy - 1) * (dy - 1) <= 2 {
511 put(dx, dy, dz, Material::Hielo); // túnel
512 }
513 }
514 }
515 }
516 }
517
518 CasaTipo::Palafito => {
519 let w = 3 + h(seed, 1, 2) as i32;
520 let d = 3 + h(seed, 2, 2) as i32;
521 let alto = 4;
522 for &(cx, cz) in &[(-w, -d), (w, -d), (-w, d), (w, d)] {
523 for dy in 0..alto {
524 put(cx, dy, cz, Material::Madera); // pilotes
525 }
526 }
527 for dx in -w..=w {
528 for dz in -d..=d {
529 put(dx, alto, dz, Material::Madera); // plataforma
530 }
531 }
532 for dy in (alto + 1)..(alto + 5) {
533 for dx in -w..=w {
534 for dz in -d..=d {
535 let borde = dx == -w || dx == w || dz == -d || dz == d;
536 if borde {
537 if dz == d && dx.abs() <= 1 && dy < alto + 3 {
538 continue; // puerta
539 }
540 let ventana = (dx == -w || dx == w) && dy == alto + 2;
541 put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Madera });
542 }
543 }
544 }
545 }
546 for dx in (-w - 1)..=(w + 1) {
547 for dz in (-d - 1)..=(d + 1) {
548 let slope = (d + 1 - dz.abs()).max(0);
549 put(dx, alto + 5 + slope, dz, Material::Paja);
550 }
551 }
552 for dy in 0..alto {
553 put(0, dy, d + 1, Material::Madera); // escalera
554 }
555 }
556
557 CasaTipo::Adobe => {
558 let w = 3 + h(seed, 1, 3) as i32;
559 let d = 3 + h(seed, 2, 3) as i32;
560 let h1 = 4;
561 for dy in 0..h1 {
562 for dx in -w..=w {
563 for dz in -d..=d {
564 let borde = dx == -w || dx == w || dz == -d || dz == d;
565 if borde {
566 if dz == d && dx.abs() <= 1 && dy < 3 {
567 continue; // puerta
568 }
569 let ventana = dy == 2 && dx.abs() == w && dz.abs() == d / 2;
570 put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Adobe });
571 }
572 }
573 }
574 }
575 for dx in -w..=w {
576 for dz in -d..=d {
577 put(dx, h1, dz, Material::Adobe); // techo plano
578 }
579 }
580 let mut vx = -w;
581 while vx <= w {
582 put(vx, h1 - 1, d + 1, Material::Madera); // vigas que sobresalen
583 put(vx, h1 - 1, -d - 1, Material::Madera);
584 vx += 2;
585 }
586 let ow = (w - 1).max(1);
587 for dy in h1..(h1 + 3) {
588 for dx in -ow..=(ow - 1) {
589 for dz in -ow..=(ow - 1) {
590 let borde = dx == -ow || dx == ow - 1 || dz == -ow || dz == ow - 1;
591 if borde {
592 put(dx, dy, dz, Material::Arcilla); // segundo cuerpo apilado
593 }
594 }
595 }
596 }
597 for dx in -ow..=(ow - 1) {
598 for dz in -ow..=(ow - 1) {
599 put(dx, h1 + 3, dz, Material::Adobe);
600 }
601 }
602 }
603
604 CasaTipo::Rascacielos => {
605 let ht = 14 + h(seed, 1, 12) as i32; // 14..25
606 let w = 2 + h(seed, 2, 2) as i32; // 2..3
607 for dy in 0..ht {
608 for dx in -w..=w {
609 for dz in -w..=w {
610 let borde = dx == -w || dx == w || dz == -w || dz == w;
611 if borde {
612 let esquina = dx.abs() == w && dz.abs() == w;
613 let banda = dy % 4 == 0;
614 put(dx, dy, dz, if esquina || banda { Material::Acero } else { Material::Vidrio });
615 }
616 }
617 }
618 }
619 for dx in -w..=w {
620 for dz in -w..=w {
621 put(dx, ht, dz, Material::Hormigon);
622 }
623 }
624 put(0, ht + 1, 0, Material::Acero);
625 put(0, ht + 2, 0, Material::Acero);
626 }
627
628 CasaTipo::Granja => {
629 let w = 3i32;
630 let d = 3i32;
631 for dy in 0..5 {
632 for dx in -w..=w {
633 for dz in -d..=d {
634 let borde = dx == -w || dx == w || dz == -d || dz == d;
635 if borde {
636 if dz == d && dx.abs() <= 1 && dy < 3 {
637 continue; // puerta
638 }
639 let ventana = dy == 2 && dx.abs() == w && dz == 0;
640 put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Yeso });
641 }
642 }
643 }
644 }
645 for dx in (-w - 1)..=(w + 1) {
646 for dz in (-d - 1)..=(d + 1) {
647 let slope = (d + 1 - dz.abs()).max(0);
648 put(dx, 5 + slope, dz, Material::Teja);
649 }
650 }
651 for dy in 0..7 {
652 put(w, dy, -d + 1, Material::Ladrillo); // chimenea
653 }
654 // Granero al costado.
655 let cx = w + 5;
656 for dy in 0..6 {
657 for dx in (cx - 3)..=(cx + 3) {
658 for dz in -3..=3 {
659 let borde = dx == cx - 3 || dx == cx + 3 || dz == -3 || dz == 3;
660 if borde {
661 if dz == 3 && (dx - cx).abs() <= 1 && dy < 4 {
662 continue; // portón
663 }
664 put(dx, dy, dz, Material::Ladrillo);
665 }
666 }
667 }
668 }
669 for dx in (cx - 4)..=(cx + 4) {
670 for dz in -4i32..=4 {
671 let slope = (4 - dz.abs()).max(0);
672 put(dx, 6 + slope, dz, Material::Teja);
673 }
674 }
675 }
676
677 CasaTipo::Torre => {
678 // Torreón de piedra: planta cuadrada, alto variable, almenas arriba.
679 let r = 2 + h(seed, 1, 2) as i32; // 2..3
680 let alto = 8 + h(seed, 2, 6) as i32; // 8..13
681 let piedra = [Material::Granito, Material::Arenisca, Material::Basalto][h(seed, 7, 3) as usize];
682 for dy in 0..alto {
683 for dx in -r..=r {
684 for dz in -r..=r {
685 let borde = dx == -r || dx == r || dz == -r || dz == r;
686 if borde {
687 if dz == r && dx == 0 && dy < 3 {
688 continue; // portón
689 }
690 // Aspilleras: ranuras verticales angostas.
691 let aspillera = dy > 2 && dy % 3 == 0 && (dx == 0 || dz == 0);
692 put(dx, dy, dz, if aspillera { Material::Vidrio } else { piedra });
693 }
694 }
695 }
696 }
697 // Almenas: dientes alternados en el borde superior.
698 for dx in -r..=r {
699 for dz in -r..=r {
700 let borde = dx == -r || dx == r || dz == -r || dz == r;
701 if borde && (dx + dz).rem_euclid(2) == 0 {
702 put(dx, alto, dz, piedra);
703 }
704 }
705 }
706 }
707
708 CasaTipo::Faro => {
709 // Faro: torre cilíndrica con franjas blancas y rojas + linterna acristalada.
710 let alto = 12 + h(seed, 1, 6) as i32; // 12..17
711 let r = 3;
712 for dy in 0..alto {
713 let banda = (dy / 3) % 2 == 0;
714 let piel = if banda { Material::Yeso } else { Material::Ladrillo };
715 for dx in -r..=r {
716 for dz in -r..=r {
717 // Aproximación de círculo por radio.
718 let dist2 = dx * dx + dz * dz;
719 let anillo = dist2 <= r * r && dist2 > (r - 1) * (r - 1);
720 if anillo {
721 if dz == r - 0 && dx == 0 && dy < 3 {
722 continue; // puerta
723 }
724 put(dx, dy, dz, piel);
725 }
726 }
727 }
728 }
729 // Galería + linterna de vidrio.
730 for dx in -r..=r {
731 for dz in -r..=r {
732 if dx * dx + dz * dz <= r * r {
733 put(dx, alto, dz, Material::Acero);
734 }
735 }
736 }
737 for dy in (alto + 1)..(alto + 4) {
738 for dx in -(r - 1)..=(r - 1) {
739 for dz in -(r - 1)..=(r - 1) {
740 let dist2 = dx * dx + dz * dz;
741 if dist2 <= (r - 1) * (r - 1) && dist2 > (r - 2).max(0) * (r - 2).max(0) {
742 put(dx, dy, dz, Material::Vidrio);
743 }
744 }
745 }
746 }
747 put(0, alto + 4, 0, Material::Bloom); // remate de la linterna
748 }
749
750 CasaTipo::Choza => {
751 // Choza tropical: paredes bajas de adobe/madera + techo cónico de paja.
752 let r = 2 + h(seed, 1, 2) as i32; // 2..3
753 let pared = if h(seed, 5, 2) == 0 { Material::Adobe } else { Material::Madera };
754 for dy in 0..3 {
755 for dx in -r..=r {
756 for dz in -r..=r {
757 let borde = dx == -r || dx == r || dz == -r || dz == r;
758 if borde {
759 if dz == r && dx == 0 && dy < 2 {
760 continue; // vano
761 }
762 put(dx, dy, dz, pared);
763 }
764 }
765 }
766 }
767 // Techo cónico de paja: capas que se cierran hacia la punta.
768 let mut rr = r + 1;
769 let mut dy = 3;
770 while rr >= 0 {
771 for dx in -rr..=rr {
772 for dz in -rr..=rr {
773 if dx.abs() == rr || dz.abs() == rr || rr == 0 {
774 put(dx, dy, dz, Material::Paja);
775 }
776 }
777 }
778 rr -= 1;
779 dy += 1;
780 }
781 }
782
783 CasaTipo::Tienda => {
784 // Local a pie de calle: caja baja, vidriera al frente, toldo y cartel.
785 let w = 4 + h(seed, 1, 3) as i32;
786 let d = 3 + h(seed, 2, 2) as i32;
787 let alto = 4;
788 let muro = [Material::Ladrillo, Material::Yeso, Material::Arcilla][h(seed, 7, 3) as usize];
789 for dy in 0..alto {
790 for dx in -w..=w {
791 for dz in -d..=d {
792 let borde = dx == -w || dx == w || dz == -d || dz == d;
793 if borde {
794 // Frente (dz == d): vidriera grande + puerta.
795 if dz == d && dy < 3 {
796 if dx.abs() <= w - 1 {
797 put(dx, dy, dz, Material::Vidrio);
798 }
799 continue;
800 }
801 put(dx, dy, dz, muro);
802 }
803 }
804 }
805 }
806 // Techo plano + cartel.
807 for dx in -w..=w {
808 for dz in -d..=d {
809 put(dx, alto, dz, Material::Hormigon);
810 }
811 }
812 // Toldo: alero rayado sobre la vidriera.
813 let toldo = [Material::Ladrillo, Material::Bloom, Material::Azul][h(seed, 8, 3) as usize];
814 for dx in -w..=w {
815 put(dx, 3, d + 1, if dx.rem_euclid(2) == 0 { toldo } else { Material::Yeso });
816 }
817 // Cartel sobre el toldo.
818 for dx in -(w - 1)..=(w - 1) {
819 put(dx, alto + 1, d, muro);
820 }
821 }
822
823 CasaTipo::Duplex => {
824 // Dúplex suburbano: dos plantas, techo a dos aguas, garaje anexo.
825 let w = 4 + h(seed, 1, 2) as i32;
826 let d = 4 + h(seed, 2, 2) as i32;
827 let alto = 8; // dos plantas de 4
828 let muro = [Material::Yeso, Material::Ladrillo, Material::Arcilla][h(seed, 7, 3) as usize];
829 for dy in 0..alto {
830 for dx in -w..=w {
831 for dz in -d..=d {
832 let borde = dx == -w || dx == w || dz == -d || dz == d;
833 if borde {
834 let puerta = dz == d && dx.abs() <= 1 && dy < 3;
835 if puerta {
836 continue;
837 }
838 // Ventanas: una por planta a media altura.
839 let ventana = (dy == 2 || dy == 6) && (dx.abs() == 2 || dz.abs() == 2);
840 put(dx, dy, dz, if ventana { Material::Vidrio } else { muro });
841 }
842 }
843 }
844 }
845 // Techo a dos aguas de teja.
846 for dx in (-w - 1)..=(w + 1) {
847 for dz in (-d - 1)..=(d + 1) {
848 let slope = (d + 1 - dz.abs()).max(0);
849 put(dx, alto + slope, dz, Material::Teja);
850 }
851 }
852 // Garaje anexo bajo.
853 let gx = w + 4;
854 for dy in 0..4 {
855 for dx in (gx - 3)..=(gx + 3) {
856 for dz in -3..=3 {
857 let borde = dx == gx - 3 || dx == gx + 3 || dz == -3 || dz == 3;
858 if borde {
859 if dz == 3 && (dx - gx).abs() <= 2 && dy < 3 {
860 continue; // portón del garaje
861 }
862 put(dx, dy, dz, muro);
863 }
864 }
865 }
866 }
867 for dx in (gx - 3)..=(gx + 3) {
868 for dz in -3..=3 {
869 put(dx, 4, dz, Material::Hormigon);
870 }
871 }
872 }
873
874 CasaTipo::Palacio => palacio(g, (bx, by, bz), seed),
875 CasaTipo::Kiosko => kiosko(g, (bx, by, bz), seed),
876 }
877}
878
879/// Un color con su brillo, de un material o de un mineral con nombre.
880type Tinte = ([u8; 3], f32);
881
882fn de_mineral(m: Mineral) -> Tinte {
883 (m.color(), m.brillo())
884}
885
886fn de_material(m: Material) -> Tinte {
887 (m.color(), m.brillo())
888}
889
890/// Pone un voxel relativo a la base, si cae en la grilla — y su espejo en `x`,
891/// por la misma regla que el `put` de [`stamp_casa`]: toda construcción es
892/// simétrica izquierda-derecha.
893fn pon(g: &mut VoxelGrid, base: (i32, i32, i32), dx: i32, dy: i32, dz: i32, t: Tinte) {
894 let dim = g.dim();
895 for dx in [dx, -dx] {
896 let (px, py, pz) = (base.0 + dx, base.1 + dy, base.2 + dz);
897 if px >= 0 && py >= 0 && pz >= 0 && (px as u32) < dim[0] && (py as u32) < dim[1] && (pz as u32) < dim[2] {
898 g.set_rgba(px as u32, py as u32, pz as u32, t.0, VoxelGrid::shine_to_alpha(t.1));
899 }
900 }
901}
902
903/// El palacio: escalinata de mármol, cuerpo de mármol con ventanales de
904/// diamante y portal de oro, cuatro torres con aguja de diamante, cúpula de
905/// diamante y remate de oro. El frente mira a `+Z`, como las demás.
906fn palacio(g: &mut VoxelGrid, b: (i32, i32, i32), seed: u32) {
907 let marmol = de_material(Material::Marmol);
908 let diamante = de_mineral(Mineral::Diamante);
909 let oro = de_mineral(Mineral::Oro);
910 // Grande: el rey y la princesa miden unos cuatro voxels, y un palacio que
911 // les llega a la cintura de las torres se lee como una casa con agujas.
912 let w = 15 + h(seed, 1, 2) as i32; // medio ancho del cuerpo: 15..16
913 let d = 12;
914 let alto = 12 + h(seed, 2, 2) as i32; // 12..13
915 // Escalinata: dos gradas de mármol más anchas que el cuerpo, la de abajo
916 // asoma al frente.
917 for (dy, extra, frente) in [(0, 3, 4), (1, 2, 2)] {
918 for dx in -(w + extra)..=(w + extra) {
919 for dz in -(d + extra)..=(d + frente) {
920 pon(g, b, dx, dy, dz, marmol);
921 }
922 }
923 }
924 let y0 = 2;
925 // Cuerpo: muros de mármol; ventanales de diamante a media altura; portal
926 // de tres de ancho con marco de oro.
927 for dy in 0..alto {
928 for dx in -w..=w {
929 for dz in -d..=d {
930 let borde = dx == -w || dx == w || dz == -d || dz == d;
931 if !borde {
932 continue;
933 }
934 let portal = dz == d && dx.abs() <= 2 && dy < 6;
935 if portal {
936 continue;
937 }
938 let marco = dz == d && dx.abs() == 3 && dy < 7 || dz == d && dx.abs() <= 3 && dy == 6;
939 // Dos hileras de ventanales: la planta baja y la alta.
940 let fila = (3..=5).contains(&dy) || (8..=10).contains(&dy);
941 let ventana = fila && (dx.rem_euclid(3) == 0 || dz.rem_euclid(3) == 0) && !(dx.abs() == w && dz.abs() == d);
942 let t = if marco { oro } else if ventana { diamante } else { marmol };
943 pon(g, b, dx, y0 + dy, dz, t);
944 }
945 }
946 }
947 // Techo plano de mármol con cornisa de oro.
948 for dx in -(w + 1)..=(w + 1) {
949 for dz in -(d + 1)..=(d + 1) {
950 let cornisa = dx.abs() == w + 1 || dz.abs() == d + 1;
951 pon(g, b, dx, y0 + alto, dz, if cornisa { oro } else { marmol });
952 }
953 }
954 // Cuatro torres en las esquinas, más altas que el cuerpo, con aguja de diamante.
955 let r = 3;
956 let alto_torre = alto + 8;
957 for (sx, sz) in [(-1, -1), (1, -1), (-1, 1), (1, 1)] {
958 let (cx, cz) = (sx * w, sz * d);
959 for dy in 0..alto_torre {
960 for dx in -r..=r {
961 for dz in -r..=r {
962 if dx * dx + dz * dz <= r * r + 1 {
963 let t = if dy == alto_torre - 1 { oro } else { marmol };
964 pon(g, b, cx + dx, y0 + dy, cz + dz, t);
965 }
966 }
967 }
968 }
969 // Aguja: cono de diamante que se afina en seis escalones.
970 for k in 0..6 {
971 let rr = (r - k / 2).max(0);
972 for dx in -rr..=rr {
973 for dz in -rr..=rr {
974 if dx * dx + dz * dz <= rr * rr {
975 pon(g, b, cx + dx, y0 + alto_torre + k, cz + dz, diamante);
976 }
977 }
978 }
979 }
980 pon(g, b, cx, y0 + alto_torre + 6, cz, oro);
981 }
982 // Cúpula central de diamante sobre el techo, con remate de oro.
983 let rc = 8;
984 let yc = y0 + alto + 1;
985 for dx in -rc..=rc {
986 for dy in 0..=rc {
987 for dz in -rc..=rc {
988 let dd = dx * dx + dy * dy + dz * dz;
989 if dd <= rc * rc && dd > (rc - 1) * (rc - 1) {
990 pon(g, b, dx, yc + dy, dz, diamante);
991 }
992 }
993 }
994 }
995 pon(g, b, 0, yc + rc + 1, 0, oro);
996 pon(g, b, 0, yc + rc + 2, 0, oro);
997}
998
999/// El kiosko: piso de mármol, ocho columnas y un domo bajo de malaquita (franjas
1000/// de cobre y jade, que es lo que la malaquita es), con borde y remate de oro.
1001/// Abierto: se ve a través y se puede estar adentro.
1002fn kiosko(g: &mut VoxelGrid, b: (i32, i32, i32), seed: u32) {
1003 let marmol = de_material(Material::Marmol);
1004 let oro = de_mineral(Mineral::Oro);
1005 let malaquita = |dy: i32| de_mineral(if dy.rem_euclid(2) == 0 { Mineral::Cobre } else { Mineral::Jade });
1006 let r = 6 + h(seed, 1, 2) as i32; // 6..7
1007 // Piso redondo de mármol, una grada.
1008 for dx in -(r + 1)..=(r + 1) {
1009 for dz in -(r + 1)..=(r + 1) {
1010 if dx * dx + dz * dz <= (r + 1) * (r + 1) {
1011 pon(g, b, dx, 0, dz, marmol);
1012 }
1013 }
1014 }
1015 // Ocho columnas en el borde.
1016 let alto = 6;
1017 for i in 0..8 {
1018 let a = i as f32 * std::f32::consts::TAU / 8.0;
1019 let (cx, cz) = ((a.cos() * r as f32).round() as i32, (a.sin() * r as f32).round() as i32);
1020 for dy in 1..=alto {
1021 pon(g, b, cx, dy, cz, malaquita(dy));
1022 }
1023 }
1024 // Aro de oro arriba de las columnas.
1025 for dx in -r..=r {
1026 for dz in -r..=r {
1027 let dd = dx * dx + dz * dz;
1028 if dd <= r * r && dd > (r - 1) * (r - 1) {
1029 pon(g, b, dx, alto + 1, dz, oro);
1030 }
1031 }
1032 }
1033 // Domo bajo (media esfera achatada) de malaquita en franjas.
1034 for dx in -r..=r {
1035 for dy in 0..=r {
1036 for dz in -r..=r {
1037 let dd = dx * dx + (dy * 2) * (dy * 2) + dz * dz;
1038 if dd <= r * r && dd > (r - 1) * (r - 1) {
1039 pon(g, b, dx, alto + 2 + dy, dz, malaquita(dy));
1040 }
1041 }
1042 }
1043 }
1044 let cima = alto + 2 + r / 2 + 1;
1045 pon(g, b, 0, cima, 0, oro);
1046 pon(g, b, 0, cima + 1, 0, oro);
1047}
1048
1049/// **Planta una casa a mano** en la columna `(x, z)` de la grilla: busca el
1050/// suelo, despeja lo que haya encima en su [`huella`](CasaTipo::huella) (árboles,
1051/// pasto, otra casa) y rellena lo que falte abajo, así la casa queda apoyada en
1052/// un piso parejo, y después la estampa. Es lo que usa un decorado de escena:
1053/// la generación procedural nivela su propio casco urbano, pero una casa
1054/// puesta en un punto cualquiera cae sobre el relieve que haya.
1055///
1056/// Devuelve la `y` del piso, o `None` si la columna no tiene suelo.
1057pub fn plantar_casa(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, z: u32, seed: u32) -> Option<u32> {
1058 plantar_casa_girada(g, tipo, x, z, seed, 0)
1059}
1060
1061/// Igual que [`plantar_casa`], con el frente girado `giro` cuartos de vuelta
1062/// alrededor de su centro (`0` = a `+Z`, `1` = a `+X`, `2` = a `-Z`, `3` = a
1063/// `-X`). La casa se genera en una grilla aparte y se copia rotada: el
1064/// generador de cada estilo sigue escribiendo «de frente a +Z» sin saber nada.
1065pub fn plantar_casa_girada(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, z: u32, seed: u32, giro: u8) -> Option<u32> {
1066 let dim = g.dim();
1067 if x >= dim[0] || z >= dim[2] {
1068 return None;
1069 }
1070 let suelo = g.height_at(x, z)?;
1071 let piso = suelo + 1;
1072 let r = tipo.huella();
1073 for dx in -r..=r {
1074 for dz in -r..=r {
1075 if dx * dx + dz * dz > r * r {
1076 continue;
1077 }
1078 let (px, pz) = (x as i32 + dx, z as i32 + dz);
1079 if px < 0 || pz < 0 || px as u32 >= dim[0] || pz as u32 >= dim[2] {
1080 continue;
1081 }
1082 let (px, pz) = (px as u32, pz as u32);
1083 // El material del suelo de esa columna, para rellenar con lo mismo.
1084 let tope = g.height_at(px, pz);
1085 let relleno = tope.and_then(|y| g.get(px, y, pz)).unwrap_or([120, 110, 100, 255]);
1086 for y in piso..dim[1] {
1087 g.clear(px, y, pz);
1088 }
1089 let desde = tope.map(|y| y + 1).unwrap_or(0);
1090 for y in desde..piso {
1091 g.set_rgba(px, y, pz, [relleno[0], relleno[1], relleno[2]], relleno[3]);
1092 }
1093 }
1094 }
1095 let giro = giro % 4;
1096 if giro == 0 {
1097 stamp_casa(g, tipo, x, piso, z, seed);
1098 return Some(piso);
1099 }
1100 // Una grilla de la huella entera (más un margen: algún estilo asoma un
1101 // voxel de su huella), con el centro en el medio.
1102 let m = r + 4;
1103 let lado = (2 * m + 1) as u32;
1104 let mut aparte = VoxelGrid::new([lado, dim[1], lado]);
1105 stamp_casa(&mut aparte, tipo, m as u32, piso, m as u32, seed);
1106 for ax in 0..lado {
1107 for az in 0..lado {
1108 let (dx, dz) = (ax as i32 - m, az as i32 - m);
1109 // `+Z` gira a `+X` con un cuarto de vuelta: (dx, dz) → (dz, -dx).
1110 let (rx, rz) = match giro {
1111 1 => (dz, -dx),
1112 2 => (-dx, -dz),
1113 _ => (-dz, dx),
1114 };
1115 let (px, pz) = (x as i32 + rx, z as i32 + rz);
1116 if px < 0 || pz < 0 || px as u32 >= dim[0] || pz as u32 >= dim[2] {
1117 continue;
1118 }
1119 for y in 0..dim[1] {
1120 if let Some(c) = aparte.get(ax, y, az).filter(|c| c[3] > 0) {
1121 g.set_rgba(px as u32, y, pz as u32, [c[0], c[1], c[2]], c[3]);
1122 }
1123 }
1124 }
1125 }
1126 Some(piso)
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131 use super::*;
1132
1133 fn solids(g: &VoxelGrid, dim: [u32; 3]) -> usize {
1134 let mut n = 0;
1135 for x in 0..dim[0] {
1136 for y in 0..dim[1] {
1137 for z in 0..dim[2] {
1138 if g.is_solid(x as i32, y as i32, z as i32) {
1139 n += 1;
1140 }
1141 }
1142 }
1143 }
1144 n
1145 }
1146
1147 #[test]
1148 fn cada_tipo_genera_una_casa() {
1149 let dim = [40u32, 40, 40];
1150 for tipo in CasaTipo::ALL {
1151 let mut g = VoxelGrid::new(dim);
1152 stamp_casa(&mut g, tipo, 20, 1, 20, 7);
1153 assert!(solids(&g, dim) >= 30, "{} generó muy poco", tipo.label());
1154 }
1155 }
1156
1157 #[test]
1158 fn misma_semilla_misma_casa_distinta_semilla_distinta() {
1159 let dim = [40u32, 40, 40];
1160 let build = |seed| {
1161 let mut g = VoxelGrid::new(dim);
1162 stamp_casa(&mut g, CasaTipo::Cabana, 20, 1, 20, seed);
1163 solids(&g, dim)
1164 };
1165 assert_eq!(build(3), build(3), "misma semilla → misma casa");
1166 // Con muchas semillas, al menos algunas dan tamaños distintos.
1167 let n: std::collections::BTreeSet<usize> = (0..8).map(build).collect();
1168 assert!(n.len() > 1, "distintas semillas dan casas distintas");
1169 }
1170
1171 /// **Toda construcción es simétrica**: el lado izquierdo igual que el
1172 /// derecho, visto de frente (el frente mira a `+Z`, así que el espejo es en
1173 /// `x`). Así construyen los humanos, y el operador lo puso como regla sin
1174 /// excepción: una casa torcida se lee como un error del generador, no como
1175 /// un estilo. Se mide voxel por voxel, color incluido, y con varias semillas
1176 /// porque la asimetría suele venir de una variación al azar.
1177 #[test]
1178 fn toda_casa_es_simetrica_izquierda_derecha() {
1179 let dim = [72u32, 96, 72];
1180 let cx = 36u32;
1181 let mut malas = Vec::new();
1182 for tipo in CasaTipo::ALL {
1183 for seed in 0..6 {
1184 let mut g = VoxelGrid::new(dim);
1185 stamp_casa(&mut g, tipo, cx, 1, 36, seed);
1186 let mut distintos = 0;
1187 for x in 0..dim[0] {
1188 let espejo = 2 * cx as i64 - x as i64;
1189 if !(0..dim[0] as i64).contains(&espejo) {
1190 continue;
1191 }
1192 for y in 0..dim[1] {
1193 for z in 0..dim[2] {
1194 let a = g.get(x, y, z).filter(|c| c[3] > 0);
1195 let b = g.get(espejo as u32, y, z).filter(|c| c[3] > 0);
1196 if a != b {
1197 distintos += 1;
1198 }
1199 }
1200 }
1201 }
1202 if distintos > 0 {
1203 malas.push(format!("{} (semilla {seed}): {distintos} voxels sin espejo", tipo.label()));
1204 }
1205 }
1206 }
1207 assert!(malas.is_empty(), "construcciones asimétricas:\n{}", malas.join("\n"));
1208 }
1209
1210 #[test]
1211 fn los_presets_de_pueblo_traen_casas_y_densidad() {
1212 for pt in PuebloTipo::ALL {
1213 let casas = pt.casas();
1214 assert!(!casas.is_empty(), "{} sin casas", pt.label());
1215 assert!(casas.iter().all(|c| c.densidad > 0.0), "{} con densidad 0", pt.label());
1216 let d = pt.densidad();
1217 assert!((0.05..=0.95).contains(&d), "{} densidad fuera de rango: {d}", pt.label());
1218 }
1219 // La metrópolis es más densa que la aldea.
1220 assert!(PuebloTipo::Metropolis.densidad() > PuebloTipo::Aldea.densidad());
1221 }
1222}