gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 llimphi-voxel/src/casa.rs Rust · 895 líneas · 34.4 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

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::worldgen::Material;
10use llimphi_3d::VoxelGrid;
11
12/// Estilo de casa. Cada uno tiene su gramática de generación.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum CasaTipo {
15    /// Prehistórica (Flintstones): choza de piedra abovedada, puerta en arco.
16    Flintstone,
17    /// Hobbit: montículo de pasto con puerta y ventana redondas.
18    Hobbit,
19    /// Cabaña de troncos: muros de madera, techo a dos aguas, chimenea de piedra.
20    Cabana,
21    /// Casa moderna: hormigón y grandes ventanales de vidrio, techo plano.
22    Moderna,
23    /// Edificio de departamentos: bloque alto con grilla de ventanas.
24    Edificio,
25    /// Iglú: cúpula de nieve con túnel de entrada.
26    Iglu,
27    /// Palafito: casa de madera sobre pilotes.
28    Palafito,
29    /// Adobe (pueblo): bloques de adobe apilados, techos planos y vigas.
30    Adobe,
31    /// Rascacielos: torre de vidrio y acero (altura variable).
32    Rascacielos,
33    /// Granja: casa con techo a dos aguas + granero.
34    Granja,
35    /// Torre medieval: torreón de piedra con almenas.
36    Torre,
37    /// Faro costero: torre blanca con franjas rojas y linterna que emite luz.
38    Faro,
39    /// Choza tropical: paredes bajas y techo cónico de paja.
40    Choza,
41    /// Tienda: local a pie de calle con toldo y vidriera.
42    Tienda,
43    /// Dúplex suburbano: dos plantas, techo a dos aguas, garaje.
44    Duplex,
45}
46
47impl CasaTipo {
48    pub const ALL: [CasaTipo; 15] = [
49        CasaTipo::Flintstone,
50        CasaTipo::Hobbit,
51        CasaTipo::Cabana,
52        CasaTipo::Moderna,
53        CasaTipo::Edificio,
54        CasaTipo::Iglu,
55        CasaTipo::Palafito,
56        CasaTipo::Adobe,
57        CasaTipo::Rascacielos,
58        CasaTipo::Granja,
59        CasaTipo::Torre,
60        CasaTipo::Faro,
61        CasaTipo::Choza,
62        CasaTipo::Tienda,
63        CasaTipo::Duplex,
64    ];
65
66    pub fn label(self) -> &'static str {
67        match self {
68            CasaTipo::Flintstone => "flintstone",
69            CasaTipo::Hobbit => "hobbit",
70            CasaTipo::Cabana => "cabaña",
71            CasaTipo::Moderna => "moderna",
72            CasaTipo::Edificio => "edificio",
73            CasaTipo::Iglu => "iglú",
74            CasaTipo::Palafito => "palafito",
75            CasaTipo::Adobe => "adobe",
76            CasaTipo::Rascacielos => "rascacielos",
77            CasaTipo::Granja => "granja",
78            CasaTipo::Torre => "torre",
79            CasaTipo::Faro => "faro",
80            CasaTipo::Choza => "choza",
81            CasaTipo::Tienda => "tienda",
82            CasaTipo::Duplex => "dúplex",
83        }
84    }
85
86    pub fn next(self) -> CasaTipo {
87        let i = CasaTipo::ALL.iter().position(|&t| t == self).unwrap_or(0);
88        CasaTipo::ALL[(i + 1) % CasaTipo::ALL.len()]
89    }
90}
91
92/// **Uso de casas en un bioma**: qué estilo y con qué densidad `[0,1]` aparecen
93/// (una candidata por parcela de la grilla gruesa; el bioma las esparce por semilla
94/// → un pueblo/ciudad procedural, como la flora).
95#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
96pub struct CasaUso {
97    pub tipo: CasaTipo,
98    pub densidad: f32,
99}
100
101/// **Tipo de asentamiento**: preset que rellena la mezcla de casas ([`CasaUso`])
102/// y la densidad urbana de un bioma → distintos pueblos y ciudades de un toque.
103/// Es sólo un atajo de autoría: al aplicarlo, escribe `casas` + `densidad_urbana`.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
105pub enum PuebloTipo {
106    /// Aldea: puñado de casas rústicas, muy dispersa.
107    Aldea,
108    /// Suburbio: dúplex y casas modernas en densidad media.
109    Suburbio,
110    /// Ciudad: mezcla de edificios y casas, densa.
111    Ciudad,
112    /// Metrópolis: rascacielos y torres de departamentos, muy densa.
113    Metropolis,
114    /// Prehistórico: chozas, flintstones y hobbits, dispersa.
115    Prehistorico,
116    /// Costero: faros, palafitos y chozas junto al agua.
117    Costero,
118    /// Medieval: torres, adobe y cabañas amuralladas.
119    Medieval,
120    /// Rural: granjas y cabañas muy espaciadas.
121    Rural,
122}
123
124impl PuebloTipo {
125    pub const ALL: [PuebloTipo; 8] = [
126        PuebloTipo::Aldea,
127        PuebloTipo::Suburbio,
128        PuebloTipo::Ciudad,
129        PuebloTipo::Metropolis,
130        PuebloTipo::Prehistorico,
131        PuebloTipo::Costero,
132        PuebloTipo::Medieval,
133        PuebloTipo::Rural,
134    ];
135
136    pub fn label(self) -> &'static str {
137        match self {
138            PuebloTipo::Aldea => "aldea",
139            PuebloTipo::Suburbio => "suburbio",
140            PuebloTipo::Ciudad => "ciudad",
141            PuebloTipo::Metropolis => "metrópolis",
142            PuebloTipo::Prehistorico => "prehistórico",
143            PuebloTipo::Costero => "costero",
144            PuebloTipo::Medieval => "medieval",
145            PuebloTipo::Rural => "rural",
146        }
147    }
148
149    pub fn next(self) -> PuebloTipo {
150        let i = PuebloTipo::ALL.iter().position(|&t| t == self).unwrap_or(0);
151        PuebloTipo::ALL[(i + 1) % PuebloTipo::ALL.len()]
152    }
153
154    /// Densidad urbana sugerida `[0,1]`: cuánto se agrupan las casas (cluster fbm).
155    pub fn densidad(self) -> f32 {
156        match self {
157            PuebloTipo::Aldea => 0.25,
158            PuebloTipo::Suburbio => 0.5,
159            PuebloTipo::Ciudad => 0.7,
160            PuebloTipo::Metropolis => 0.9,
161            PuebloTipo::Prehistorico => 0.3,
162            PuebloTipo::Costero => 0.4,
163            PuebloTipo::Medieval => 0.55,
164            PuebloTipo::Rural => 0.15,
165        }
166    }
167
168    /// Mezcla de casas (estilo + probabilidad por parcela) característica del tipo.
169    pub fn casas(self) -> Vec<CasaUso> {
170        let c = |tipo, densidad| CasaUso { tipo, densidad };
171        match self {
172            PuebloTipo::Aldea => vec![
173                c(CasaTipo::Cabana, 0.4),
174                c(CasaTipo::Adobe, 0.3),
175                c(CasaTipo::Granja, 0.2),
176                c(CasaTipo::Choza, 0.2),
177            ],
178            PuebloTipo::Suburbio => vec![
179                c(CasaTipo::Duplex, 0.45),
180                c(CasaTipo::Moderna, 0.4),
181                c(CasaTipo::Tienda, 0.15),
182                c(CasaTipo::Granja, 0.1),
183            ],
184            PuebloTipo::Ciudad => vec![
185                c(CasaTipo::Edificio, 0.45),
186                c(CasaTipo::Moderna, 0.3),
187                c(CasaTipo::Tienda, 0.25),
188                c(CasaTipo::Duplex, 0.2),
189            ],
190            PuebloTipo::Metropolis => vec![
191                c(CasaTipo::Rascacielos, 0.4),
192                c(CasaTipo::Edificio, 0.5),
193                c(CasaTipo::Moderna, 0.2),
194                c(CasaTipo::Tienda, 0.2),
195            ],
196            PuebloTipo::Prehistorico => vec![
197                c(CasaTipo::Flintstone, 0.4),
198                c(CasaTipo::Choza, 0.3),
199                c(CasaTipo::Hobbit, 0.25),
200            ],
201            PuebloTipo::Costero => vec![
202                c(CasaTipo::Palafito, 0.4),
203                c(CasaTipo::Choza, 0.3),
204                c(CasaTipo::Cabana, 0.2),
205                c(CasaTipo::Faro, 0.06),
206            ],
207            PuebloTipo::Medieval => vec![
208                c(CasaTipo::Adobe, 0.4),
209                c(CasaTipo::Cabana, 0.3),
210                c(CasaTipo::Torre, 0.15),
211                c(CasaTipo::Granja, 0.2),
212            ],
213            PuebloTipo::Rural => vec![
214                c(CasaTipo::Granja, 0.4),
215                c(CasaTipo::Cabana, 0.3),
216                c(CasaTipo::Choza, 0.15),
217            ],
218        }
219    }
220}
221
222/// Deriva una semilla de casa estable de una parcela de mundo `(cwx, cwz)` y la
223/// semilla del mundo → cada parcela genera una casa distinta pero determinista.
224pub fn casa_seed(world_seed: u32, cwx: i32, cwz: i32) -> u32 {
225    world_seed
226        .wrapping_add((cwx as u32).wrapping_mul(0x9E37_79B9))
227        .wrapping_add((cwz as u32).wrapping_mul(0x85EB_CA77))
228        ^ 0xCA5A
229}
230
231/// Hash entero determinista `[0, m)` a partir de `(seed, salt)`.
232#[inline]
233fn h(seed: u32, salt: u32, m: u32) -> u32 {
234    let mut x = seed.wrapping_mul(0x9E37_79B9).wrapping_add(salt.wrapping_mul(0x85EB_CA77));
235    x ^= x >> 15;
236    x = x.wrapping_mul(0x2C1B_3C6D);
237    x ^= x >> 13;
238    x % m.max(1)
239}
240
241/// Ruido `[0,1)` determinista para jitter fino.
242#[inline]
243fn hf(seed: u32, a: i32, b: i32) -> f32 {
244    let mut x = seed
245        .wrapping_add((a as u32).wrapping_mul(0x9E37_79B9))
246        .wrapping_add((b as u32).wrapping_mul(0x85EB_CA77));
247    x ^= x >> 15;
248    x = x.wrapping_mul(0x2C1B_3C6D);
249    x ^= x >> 12;
250    (x & 0xFFFF) as f32 / 65536.0
251}
252
253/// **Genera y estampa una casa** del `tipo` con la base (esquina/centro) en
254/// `(x, y, z)`, variada por `seed`. El piso arranca en `y`.
255pub fn stamp_casa(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, y: u32, z: u32, seed: u32) {
256    let bx = x as i32;
257    let by = y as i32;
258    let bz = z as i32;
259    let dim = g.dim();
260    let mut put = |dx: i32, dy: i32, dz: i32, mat: Material| {
261        let (px, py, pz) = (bx + dx, by + dy, bz + dz);
262        if px >= 0 && py >= 0 && pz >= 0 && (px as u32) < dim[0] && (py as u32) < dim[1] && (pz as u32) < dim[2] {
263            g.set_rgba(px as u32, py as u32, pz as u32, mat.color(), VoxelGrid::shine_to_alpha(mat.brillo()));
264        }
265    };
266
267    match tipo {
268        CasaTipo::Flintstone => {
269            // Choza de piedra abovedada, con silueta lumpy y arco de entrada.
270            let w = 4 + h(seed, 1, 3) as i32; // 4..6 (medio ancho)
271            let d = 4 + h(seed, 2, 3) as i32;
272            let base_h = 4 + h(seed, 3, 2) as i32;
273            let piedra = [Material::Arenisca, Material::Grava, Material::Rock][h(seed, 9, 3) as usize];
274            for dx in -w..=w {
275                for dz in -d..=d {
276                    let borde = dx == -w || dx == w || dz == -d || dz == d;
277                    if borde {
278                        // Altura de muro con bulto (piedra irregular).
279                        let bulto = (hf(seed, dx, dz) * 2.5) as i32;
280                        let alto = base_h + bulto;
281                        for dy in 0..alto {
282                            // Arco de puerta en el frente (dz = d).
283                            let puerta = dz == d && dx.abs() <= 1 && dy < 3;
284                            if !puerta {
285                                put(dx, dy, dz, piedra);
286                            }
287                        }
288                    }
289                }
290            }
291            // Techo abovedado (cúpula chata de piedra/paja).
292            let techo = if h(seed, 4, 2) == 0 { Material::Paja } else { piedra };
293            for dx in -w..=w {
294                for dz in -d..=d {
295                    let r = (dx * dx) as f32 / (w * w) as f32 + (dz * dz) as f32 / (d * d) as f32;
296                    let cima = ((1.0 - r) * 3.0) as i32;
297                    if cima >= 0 {
298                        put(dx, base_h + cima, dz, techo);
299                    }
300                }
301            }
302        }
303
304        CasaTipo::Hobbit => {
305            // Montículo de pasto con puerta y ventana redondas.
306            let r = 5 + h(seed, 1, 3) as i32; // radio 5..7
307            let past = [Material::Grass, Material::Musgo, Material::Leaf][h(seed, 8, 3) as usize];
308            for dx in -r..=r {
309                for dy in 0..=r {
310                    for dz in -r..=r {
311                        let dd = dx * dx + dy * dy + dz * dz;
312                        // Cáscara de la media esfera (montículo).
313                        if dd <= r * r && dd > (r - 2) * (r - 2) {
314                            put(dx, dy, dz, past);
315                        }
316                    }
317                }
318            }
319            // Fachada + puerta redonda al frente (dz = r-1..r).
320            let zf = r;
321            for dx in -2..=2 {
322                for dy in 0..4 {
323                    if dx * dx + (dy - 1) * (dy - 1) <= 4 {
324                        put(dx, dy, zf - 1, Material::Ladrillo); // marco de la puerta
325                    }
326                }
327            }
328            for dx in -1..=1 {
329                for dy in 0..3 {
330                    put(dx, dy, zf, Material::Madera); // puerta redonda de madera
331                }
332            }
333            put(3, 2, zf - 1, Material::Vidrio); // ventanita redonda
334            put(-3, 2, zf - 1, Material::Vidrio);
335            // Chimenea.
336            put(0, r + 1, 0, Material::Ladrillo);
337            put(0, r + 2, 0, Material::Ladrillo);
338        }
339
340        CasaTipo::Cabana => {
341            // Cabaña de troncos con techo a dos aguas y chimenea de piedra.
342            let w = 3 + h(seed, 1, 3) as i32; // 3..5
343            let d = 4 + h(seed, 2, 3) as i32; // 4..6
344            let wall = 5;
345            for dy in 0..wall {
346                for dx in -w..=w {
347                    for dz in -d..=d {
348                        let borde = dx == -w || dx == w || dz == -d || dz == d;
349                        if borde {
350                            // Puerta al frente + ventanas.
351                            let puerta = dz == d && dx.abs() <= 1 && dy < 3;
352                            let ventana = (dx == -w || dx == w) && dy == 2 && dz.abs() == d / 2;
353                            let m = if puerta {
354                                continue;
355                            } else if ventana {
356                                Material::Vidrio
357                            } else if dy % 2 == 0 {
358                                Material::Madera // troncos (franjas)
359                            } else {
360                                Material::Adobe
361                            };
362                            put(dx, dy, dz, m);
363                        }
364                    }
365                }
366            }
367            // Techo a dos aguas (cumbrera a lo largo del eje más largo).
368            let teja = if h(seed, 4, 2) == 0 { Material::Teja } else { Material::Pizarra };
369            let ridge_x = w >= d;
370            for dx in -w - 1..=w + 1 {
371                for dz in -d - 1..=d + 1 {
372                    let slope = if ridge_x { d + 1 - dz.abs() } else { w + 1 - dx.abs() }.max(0);
373                    put(dx, wall + slope, dz, teja);
374                }
375            }
376            // Chimenea de piedra a un lado (varía).
377            let cs = if h(seed, 5, 2) == 0 { -w } else { w };
378            for dy in 0..wall + d + 2 {
379                put(cs, dy, -d + 1, Material::Rock);
380            }
381        }
382
383        CasaTipo::Moderna => {
384            // Casa moderna: hormigón + grandes ventanales, techo plano, 1–2 plantas.
385            let w = 4 + h(seed, 1, 3) as i32;
386            let d = 4 + h(seed, 2, 3) as i32;
387            let plantas = 1 + h(seed, 3, 2) as i32; // 1..2
388            let ph = 5; // alto de planta
389            let top = plantas * ph;
390            for dy in 0..top {
391                for dx in -w..=w {
392                    for dz in -d..=d {
393                        let borde = dx == -w || dx == w || dz == -d || dz == d;
394                        if borde {
395                            let piso_line = dy % ph == 0;
396                            // Ventanal: franjas de vidrio entre líneas de piso.
397                            let ventanal = !piso_line && (dy % ph) != ph - 1;
398                            let puerta = dz == d && dx.abs() <= 1 && dy < 3;
399                            if puerta {
400                                continue;
401                            }
402                            let m = if piso_line {
403                                Material::Hormigon
404                            } else if ventanal {
405                                Material::Vidrio
406                            } else {
407                                Material::Yeso
408                            };
409                            put(dx, dy, dz, m);
410                        }
411                    }
412                }
413            }
414            // Losa/techo plano.
415            for dx in -w..=w {
416                for dz in -d..=d {
417                    put(dx, top, dz, Material::Hormigon);
418                }
419            }
420            // Parapeto + a veces un panel solar.
421            if h(seed, 6, 2) == 0 {
422                put(0, top + 1, 0, Material::Azul);
423                put(1, top + 1, 0, Material::Azul);
424            }
425        }
426
427        CasaTipo::Edificio => {
428            // Bloque de departamentos: alto, con grilla de ventanas.
429            let w = 3 + h(seed, 1, 3) as i32;
430            let d = 3 + h(seed, 2, 2) as i32;
431            let pisos = 5 + h(seed, 3, 5) as i32; // 5..9
432            let ph = 3;
433            let top = pisos * ph;
434            let muro = [Material::Ladrillo, Material::Hormigon, Material::Arcilla][h(seed, 7, 3) as usize];
435            for dy in 0..top {
436                for dx in -w..=w {
437                    for dz in -d..=d {
438                        let borde = dx == -w || dx == w || dz == -d || dz == d;
439                        if borde {
440                            // Ventana en cada piso (mitad de la altura), columnas alternas.
441                            let ventana = dy > 0 && dy % ph == 1 && (dx + dz).rem_euclid(2) == 0;
442                            let puerta = dz == d && dx.abs() <= 1 && dy < 2;
443                            if puerta {
444                                continue;
445                            }
446                            put(dx, dy, dz, if ventana { Material::Vidrio } else { muro });
447                        }
448                    }
449                }
450            }
451            for dx in -w..=w {
452                for dz in -d..=d {
453                    put(dx, top, dz, Material::Hormigon); // azotea
454                }
455            }
456            // Tanque/antena.
457            put(-w + 1, top + 1, -d + 1, Material::Acero);
458        }
459
460        CasaTipo::Iglu => {
461            let r = 4 + h(seed, 1, 2) as i32; // 4..5
462            for dx in -r..=r {
463                for dy in 0..=r {
464                    for dz in -r..=r {
465                        let dd = dx * dx + dy * dy + dz * dz;
466                        if dd <= r * r && dd > (r - 2) * (r - 2) {
467                            put(dx, dy, dz, Material::Snow); // cúpula de nieve
468                        }
469                    }
470                }
471            }
472            for dz in r..=r + 2 {
473                for dx in -1..=1 {
474                    for dy in 0..3 {
475                        if dx * dx + (dy - 1) * (dy - 1) <= 2 {
476                            put(dx, dy, dz, Material::Hielo); // túnel
477                        }
478                    }
479                }
480            }
481        }
482
483        CasaTipo::Palafito => {
484            let w = 3 + h(seed, 1, 2) as i32;
485            let d = 3 + h(seed, 2, 2) as i32;
486            let alto = 4;
487            for &(cx, cz) in &[(-w, -d), (w, -d), (-w, d), (w, d)] {
488                for dy in 0..alto {
489                    put(cx, dy, cz, Material::Madera); // pilotes
490                }
491            }
492            for dx in -w..=w {
493                for dz in -d..=d {
494                    put(dx, alto, dz, Material::Madera); // plataforma
495                }
496            }
497            for dy in (alto + 1)..(alto + 5) {
498                for dx in -w..=w {
499                    for dz in -d..=d {
500                        let borde = dx == -w || dx == w || dz == -d || dz == d;
501                        if borde {
502                            if dz == d && dx.abs() <= 1 && dy < alto + 3 {
503                                continue; // puerta
504                            }
505                            let ventana = (dx == -w || dx == w) && dy == alto + 2;
506                            put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Madera });
507                        }
508                    }
509                }
510            }
511            for dx in (-w - 1)..=(w + 1) {
512                for dz in (-d - 1)..=(d + 1) {
513                    let slope = (d + 1 - dz.abs()).max(0);
514                    put(dx, alto + 5 + slope, dz, Material::Paja);
515                }
516            }
517            for dy in 0..alto {
518                put(0, dy, d + 1, Material::Madera); // escalera
519            }
520        }
521
522        CasaTipo::Adobe => {
523            let w = 3 + h(seed, 1, 3) as i32;
524            let d = 3 + h(seed, 2, 3) as i32;
525            let h1 = 4;
526            for dy in 0..h1 {
527                for dx in -w..=w {
528                    for dz in -d..=d {
529                        let borde = dx == -w || dx == w || dz == -d || dz == d;
530                        if borde {
531                            if dz == d && dx.abs() <= 1 && dy < 3 {
532                                continue; // puerta
533                            }
534                            let ventana = dy == 2 && dx.abs() == w && dz.abs() == d / 2;
535                            put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Adobe });
536                        }
537                    }
538                }
539            }
540            for dx in -w..=w {
541                for dz in -d..=d {
542                    put(dx, h1, dz, Material::Adobe); // techo plano
543                }
544            }
545            let mut vx = -w;
546            while vx <= w {
547                put(vx, h1 - 1, d + 1, Material::Madera); // vigas que sobresalen
548                put(vx, h1 - 1, -d - 1, Material::Madera);
549                vx += 2;
550            }
551            let ow = (w - 1).max(1);
552            for dy in h1..(h1 + 3) {
553                for dx in -ow..=(ow - 1) {
554                    for dz in -ow..=(ow - 1) {
555                        let borde = dx == -ow || dx == ow - 1 || dz == -ow || dz == ow - 1;
556                        if borde {
557                            put(dx, dy, dz, Material::Arcilla); // segundo cuerpo apilado
558                        }
559                    }
560                }
561            }
562            for dx in -ow..=(ow - 1) {
563                for dz in -ow..=(ow - 1) {
564                    put(dx, h1 + 3, dz, Material::Adobe);
565                }
566            }
567        }
568
569        CasaTipo::Rascacielos => {
570            let ht = 14 + h(seed, 1, 12) as i32; // 14..25
571            let w = 2 + h(seed, 2, 2) as i32; // 2..3
572            for dy in 0..ht {
573                for dx in -w..=w {
574                    for dz in -w..=w {
575                        let borde = dx == -w || dx == w || dz == -w || dz == w;
576                        if borde {
577                            let esquina = dx.abs() == w && dz.abs() == w;
578                            let banda = dy % 4 == 0;
579                            put(dx, dy, dz, if esquina || banda { Material::Acero } else { Material::Vidrio });
580                        }
581                    }
582                }
583            }
584            for dx in -w..=w {
585                for dz in -w..=w {
586                    put(dx, ht, dz, Material::Hormigon);
587                }
588            }
589            put(0, ht + 1, 0, Material::Acero);
590            put(0, ht + 2, 0, Material::Acero);
591        }
592
593        CasaTipo::Granja => {
594            let w = 3i32;
595            let d = 3i32;
596            for dy in 0..5 {
597                for dx in -w..=w {
598                    for dz in -d..=d {
599                        let borde = dx == -w || dx == w || dz == -d || dz == d;
600                        if borde {
601                            if dz == d && dx.abs() <= 1 && dy < 3 {
602                                continue; // puerta
603                            }
604                            let ventana = dy == 2 && dx.abs() == w && dz == 0;
605                            put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Yeso });
606                        }
607                    }
608                }
609            }
610            for dx in (-w - 1)..=(w + 1) {
611                for dz in (-d - 1)..=(d + 1) {
612                    let slope = (d + 1 - dz.abs()).max(0);
613                    put(dx, 5 + slope, dz, Material::Teja);
614                }
615            }
616            for dy in 0..7 {
617                put(w, dy, -d + 1, Material::Ladrillo); // chimenea
618            }
619            // Granero al costado.
620            let cx = w + 5;
621            for dy in 0..6 {
622                for dx in (cx - 3)..=(cx + 3) {
623                    for dz in -3..=3 {
624                        let borde = dx == cx - 3 || dx == cx + 3 || dz == -3 || dz == 3;
625                        if borde {
626                            if dz == 3 && (dx - cx).abs() <= 1 && dy < 4 {
627                                continue; // portón
628                            }
629                            put(dx, dy, dz, Material::Ladrillo);
630                        }
631                    }
632                }
633            }
634            for dx in (cx - 4)..=(cx + 4) {
635                for dz in -4i32..=4 {
636                    let slope = (4 - dz.abs()).max(0);
637                    put(dx, 6 + slope, dz, Material::Teja);
638                }
639            }
640        }
641
642        CasaTipo::Torre => {
643            // Torreón de piedra: planta cuadrada, alto variable, almenas arriba.
644            let r = 2 + h(seed, 1, 2) as i32; // 2..3
645            let alto = 8 + h(seed, 2, 6) as i32; // 8..13
646            let piedra = [Material::Granito, Material::Arenisca, Material::Basalto][h(seed, 7, 3) as usize];
647            for dy in 0..alto {
648                for dx in -r..=r {
649                    for dz in -r..=r {
650                        let borde = dx == -r || dx == r || dz == -r || dz == r;
651                        if borde {
652                            if dz == r && dx == 0 && dy < 3 {
653                                continue; // portón
654                            }
655                            // Aspilleras: ranuras verticales angostas.
656                            let aspillera = dy > 2 && dy % 3 == 0 && (dx == 0 || dz == 0);
657                            put(dx, dy, dz, if aspillera { Material::Vidrio } else { piedra });
658                        }
659                    }
660                }
661            }
662            // Almenas: dientes alternados en el borde superior.
663            for dx in -r..=r {
664                for dz in -r..=r {
665                    let borde = dx == -r || dx == r || dz == -r || dz == r;
666                    if borde && (dx + dz).rem_euclid(2) == 0 {
667                        put(dx, alto, dz, piedra);
668                    }
669                }
670            }
671        }
672
673        CasaTipo::Faro => {
674            // Faro: torre cilíndrica con franjas blancas y rojas + linterna acristalada.
675            let alto = 12 + h(seed, 1, 6) as i32; // 12..17
676            let r = 3;
677            for dy in 0..alto {
678                let banda = (dy / 3) % 2 == 0;
679                let piel = if banda { Material::Yeso } else { Material::Ladrillo };
680                for dx in -r..=r {
681                    for dz in -r..=r {
682                        // Aproximación de círculo por radio.
683                        let dist2 = dx * dx + dz * dz;
684                        let anillo = dist2 <= r * r && dist2 > (r - 1) * (r - 1);
685                        if anillo {
686                            if dz == r - 0 && dx == 0 && dy < 3 {
687                                continue; // puerta
688                            }
689                            put(dx, dy, dz, piel);
690                        }
691                    }
692                }
693            }
694            // Galería + linterna de vidrio.
695            for dx in -r..=r {
696                for dz in -r..=r {
697                    if dx * dx + dz * dz <= r * r {
698                        put(dx, alto, dz, Material::Acero);
699                    }
700                }
701            }
702            for dy in (alto + 1)..(alto + 4) {
703                for dx in -(r - 1)..=(r - 1) {
704                    for dz in -(r - 1)..=(r - 1) {
705                        let dist2 = dx * dx + dz * dz;
706                        if dist2 <= (r - 1) * (r - 1) && dist2 > (r - 2).max(0) * (r - 2).max(0) {
707                            put(dx, dy, dz, Material::Vidrio);
708                        }
709                    }
710                }
711            }
712            put(0, alto + 4, 0, Material::Bloom); // remate de la linterna
713        }
714
715        CasaTipo::Choza => {
716            // Choza tropical: paredes bajas de adobe/madera + techo cónico de paja.
717            let r = 2 + h(seed, 1, 2) as i32; // 2..3
718            let pared = if h(seed, 5, 2) == 0 { Material::Adobe } else { Material::Madera };
719            for dy in 0..3 {
720                for dx in -r..=r {
721                    for dz in -r..=r {
722                        let borde = dx == -r || dx == r || dz == -r || dz == r;
723                        if borde {
724                            if dz == r && dx == 0 && dy < 2 {
725                                continue; // vano
726                            }
727                            put(dx, dy, dz, pared);
728                        }
729                    }
730                }
731            }
732            // Techo cónico de paja: capas que se cierran hacia la punta.
733            let mut rr = r + 1;
734            let mut dy = 3;
735            while rr >= 0 {
736                for dx in -rr..=rr {
737                    for dz in -rr..=rr {
738                        if dx.abs() == rr || dz.abs() == rr || rr == 0 {
739                            put(dx, dy, dz, Material::Paja);
740                        }
741                    }
742                }
743                rr -= 1;
744                dy += 1;
745            }
746        }
747
748        CasaTipo::Tienda => {
749            // Local a pie de calle: caja baja, vidriera al frente, toldo y cartel.
750            let w = 4 + h(seed, 1, 3) as i32;
751            let d = 3 + h(seed, 2, 2) as i32;
752            let alto = 4;
753            let muro = [Material::Ladrillo, Material::Yeso, Material::Arcilla][h(seed, 7, 3) as usize];
754            for dy in 0..alto {
755                for dx in -w..=w {
756                    for dz in -d..=d {
757                        let borde = dx == -w || dx == w || dz == -d || dz == d;
758                        if borde {
759                            // Frente (dz == d): vidriera grande + puerta.
760                            if dz == d && dy < 3 {
761                                if dx.abs() <= w - 1 {
762                                    put(dx, dy, dz, Material::Vidrio);
763                                }
764                                continue;
765                            }
766                            put(dx, dy, dz, muro);
767                        }
768                    }
769                }
770            }
771            // Techo plano + cartel.
772            for dx in -w..=w {
773                for dz in -d..=d {
774                    put(dx, alto, dz, Material::Hormigon);
775                }
776            }
777            // Toldo: alero rayado sobre la vidriera.
778            let toldo = [Material::Ladrillo, Material::Bloom, Material::Azul][h(seed, 8, 3) as usize];
779            for dx in -w..=w {
780                put(dx, 3, d + 1, if dx.rem_euclid(2) == 0 { toldo } else { Material::Yeso });
781            }
782            // Cartel sobre el toldo.
783            for dx in -(w - 1)..=(w - 1) {
784                put(dx, alto + 1, d, muro);
785            }
786        }
787
788        CasaTipo::Duplex => {
789            // Dúplex suburbano: dos plantas, techo a dos aguas, garaje anexo.
790            let w = 4 + h(seed, 1, 2) as i32;
791            let d = 4 + h(seed, 2, 2) as i32;
792            let alto = 8; // dos plantas de 4
793            let muro = [Material::Yeso, Material::Ladrillo, Material::Arcilla][h(seed, 7, 3) as usize];
794            for dy in 0..alto {
795                for dx in -w..=w {
796                    for dz in -d..=d {
797                        let borde = dx == -w || dx == w || dz == -d || dz == d;
798                        if borde {
799                            let puerta = dz == d && dx.abs() <= 1 && dy < 3;
800                            if puerta {
801                                continue;
802                            }
803                            // Ventanas: una por planta a media altura.
804                            let ventana = (dy == 2 || dy == 6) && (dx.abs() == 2 || dz.abs() == 2);
805                            put(dx, dy, dz, if ventana { Material::Vidrio } else { muro });
806                        }
807                    }
808                }
809            }
810            // Techo a dos aguas de teja.
811            for dx in (-w - 1)..=(w + 1) {
812                for dz in (-d - 1)..=(d + 1) {
813                    let slope = (d + 1 - dz.abs()).max(0);
814                    put(dx, alto + slope, dz, Material::Teja);
815                }
816            }
817            // Garaje anexo bajo.
818            let gx = w + 4;
819            for dy in 0..4 {
820                for dx in (gx - 3)..=(gx + 3) {
821                    for dz in -3..=3 {
822                        let borde = dx == gx - 3 || dx == gx + 3 || dz == -3 || dz == 3;
823                        if borde {
824                            if dz == 3 && (dx - gx).abs() <= 2 && dy < 3 {
825                                continue; // portón del garaje
826                            }
827                            put(dx, dy, dz, muro);
828                        }
829                    }
830                }
831            }
832            for dx in (gx - 3)..=(gx + 3) {
833                for dz in -3..=3 {
834                    put(dx, 4, dz, Material::Hormigon);
835                }
836            }
837        }
838    }
839}
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844
845    fn solids(g: &VoxelGrid, dim: [u32; 3]) -> usize {
846        let mut n = 0;
847        for x in 0..dim[0] {
848            for y in 0..dim[1] {
849                for z in 0..dim[2] {
850                    if g.is_solid(x as i32, y as i32, z as i32) {
851                        n += 1;
852                    }
853                }
854            }
855        }
856        n
857    }
858
859    #[test]
860    fn cada_tipo_genera_una_casa() {
861        let dim = [40u32, 40, 40];
862        for tipo in CasaTipo::ALL {
863            let mut g = VoxelGrid::new(dim);
864            stamp_casa(&mut g, tipo, 20, 1, 20, 7);
865            assert!(solids(&g, dim) >= 30, "{} generó muy poco", tipo.label());
866        }
867    }
868
869    #[test]
870    fn misma_semilla_misma_casa_distinta_semilla_distinta() {
871        let dim = [40u32, 40, 40];
872        let build = |seed| {
873            let mut g = VoxelGrid::new(dim);
874            stamp_casa(&mut g, CasaTipo::Cabana, 20, 1, 20, seed);
875            solids(&g, dim)
876        };
877        assert_eq!(build(3), build(3), "misma semilla → misma casa");
878        // Con muchas semillas, al menos algunas dan tamaños distintos.
879        let n: std::collections::BTreeSet<usize> = (0..8).map(build).collect();
880        assert!(n.len() > 1, "distintas semillas dan casas distintas");
881    }
882
883    #[test]
884    fn los_presets_de_pueblo_traen_casas_y_densidad() {
885        for pt in PuebloTipo::ALL {
886            let casas = pt.casas();
887            assert!(!casas.is_empty(), "{} sin casas", pt.label());
888            assert!(casas.iter().all(|c| c.densidad > 0.0), "{} con densidad 0", pt.label());
889            let d = pt.densidad();
890            assert!((0.05..=0.95).contains(&d), "{} densidad fuera de rango: {d}", pt.label());
891        }
892        // La metrópolis es más densa que la aldea.
893        assert!(PuebloTipo::Metropolis.densidad() > PuebloTipo::Aldea.densidad());
894    }
895}