tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
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}