//! **Casas procedurales por tipo**: a diferencia de un [`Mueble`](crate::Mueble) //! (modelo fijo), una casa se **genera** — dimensiones, ventanas, techo, chimenea y //! detalles varían con una **semilla**, dentro de un **estilo** ([`CasaTipo`]). //! Estampá una con [`stamp_casa`]; misma `(tipo, seed)` → misma casa (determinista), //! semillas distintas → casas distintas del mismo estilo. use serde::{Deserialize, Serialize}; use crate::mineral::Mineral; use crate::worldgen::Material; use llimphi_3d::VoxelGrid; /// Estilo de casa. Cada uno tiene su gramática de generación. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum CasaTipo { /// Prehistórica (Flintstones): choza de piedra abovedada, puerta en arco. Flintstone, /// Hobbit: montículo de pasto con puerta y ventana redondas. Hobbit, /// Cabaña de troncos: muros de madera, techo a dos aguas, chimenea de piedra. Cabana, /// Casa moderna: hormigón y grandes ventanales de vidrio, techo plano. Moderna, /// Edificio de departamentos: bloque alto con grilla de ventanas. Edificio, /// Iglú: cúpula de nieve con túnel de entrada. Iglu, /// Palafito: casa de madera sobre pilotes. Palafito, /// Adobe (pueblo): bloques de adobe apilados, techos planos y vigas. Adobe, /// Rascacielos: torre de vidrio y acero (altura variable). Rascacielos, /// Granja: casa con techo a dos aguas + granero. Granja, /// Torre medieval: torreón de piedra con almenas. Torre, /// Faro costero: torre blanca con franjas rojas y linterna que emite luz. Faro, /// Choza tropical: paredes bajas y techo cónico de paja. Choza, /// Tienda: local a pie de calle con toldo y vidriera. Tienda, /// Dúplex suburbano: dos plantas, techo a dos aguas, garaje. Duplex, /// Palacio de cuento: mármol sobre una escalinata, cuatro torres con /// agujas de diamante y una cúpula de diamante con remate de oro — «un /// lindo palacio de diamantes». Va al final: postcard numera las variantes. Palacio, /// Kiosko de jardín abierto: ocho columnas y un techo en domo, de /// malaquita (franjas de cobre y jade) con borde y remate de oro. Kiosko, } impl CasaTipo { pub const ALL: [CasaTipo; 17] = [ CasaTipo::Flintstone, CasaTipo::Hobbit, CasaTipo::Cabana, CasaTipo::Moderna, CasaTipo::Edificio, CasaTipo::Iglu, CasaTipo::Palafito, CasaTipo::Adobe, CasaTipo::Rascacielos, CasaTipo::Granja, CasaTipo::Torre, CasaTipo::Faro, CasaTipo::Choza, CasaTipo::Tienda, CasaTipo::Duplex, CasaTipo::Palacio, CasaTipo::Kiosko, ]; pub fn label(self) -> &'static str { match self { CasaTipo::Flintstone => "flintstone", CasaTipo::Hobbit => "hobbit", CasaTipo::Cabana => "cabaña", CasaTipo::Moderna => "moderna", CasaTipo::Edificio => "edificio", CasaTipo::Iglu => "iglú", CasaTipo::Palafito => "palafito", CasaTipo::Adobe => "adobe", CasaTipo::Rascacielos => "rascacielos", CasaTipo::Granja => "granja", CasaTipo::Torre => "torre", CasaTipo::Faro => "faro", CasaTipo::Choza => "choza", CasaTipo::Tienda => "tienda", CasaTipo::Duplex => "dúplex", CasaTipo::Palacio => "palacio", CasaTipo::Kiosko => "kiosko", } } /// Radio de la **huella** en planta (voxels): hasta dónde llega la casa desde /// su centro. Es lo que hay que nivelar y despejar para plantarla a mano /// ([`plantar_casa`]). Generoso: mejor despejar de más que dejar un árbol /// atravesando un muro. pub fn huella(self) -> i32 { match self { // La esquina de la escalinata está a (w+3, d+4) = (19, 16): 25 cubre // el palacio más grande y deja un voxel de aire alrededor. CasaTipo::Palacio => 25, CasaTipo::Kiosko => 9, CasaTipo::Rascacielos | CasaTipo::Granja | CasaTipo::Duplex => 12, _ => 9, } } pub fn next(self) -> CasaTipo { let i = CasaTipo::ALL.iter().position(|&t| t == self).unwrap_or(0); CasaTipo::ALL[(i + 1) % CasaTipo::ALL.len()] } } /// **Uso de casas en un bioma**: qué estilo y con qué densidad `[0,1]` aparecen /// (una candidata por parcela de la grilla gruesa; el bioma las esparce por semilla /// → un pueblo/ciudad procedural, como la flora). #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct CasaUso { pub tipo: CasaTipo, pub densidad: f32, } /// **Tipo de asentamiento**: preset que rellena la mezcla de casas ([`CasaUso`]) /// y la densidad urbana de un bioma → distintos pueblos y ciudades de un toque. /// Es sólo un atajo de autoría: al aplicarlo, escribe `casas` + `densidad_urbana`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum PuebloTipo { /// Aldea: puñado de casas rústicas, muy dispersa. Aldea, /// Suburbio: dúplex y casas modernas en densidad media. Suburbio, /// Ciudad: mezcla de edificios y casas, densa. Ciudad, /// Metrópolis: rascacielos y torres de departamentos, muy densa. Metropolis, /// Prehistórico: chozas, flintstones y hobbits, dispersa. Prehistorico, /// Costero: faros, palafitos y chozas junto al agua. Costero, /// Medieval: torres, adobe y cabañas amuralladas. Medieval, /// Rural: granjas y cabañas muy espaciadas. Rural, } impl PuebloTipo { pub const ALL: [PuebloTipo; 8] = [ PuebloTipo::Aldea, PuebloTipo::Suburbio, PuebloTipo::Ciudad, PuebloTipo::Metropolis, PuebloTipo::Prehistorico, PuebloTipo::Costero, PuebloTipo::Medieval, PuebloTipo::Rural, ]; pub fn label(self) -> &'static str { match self { PuebloTipo::Aldea => "aldea", PuebloTipo::Suburbio => "suburbio", PuebloTipo::Ciudad => "ciudad", PuebloTipo::Metropolis => "metrópolis", PuebloTipo::Prehistorico => "prehistórico", PuebloTipo::Costero => "costero", PuebloTipo::Medieval => "medieval", PuebloTipo::Rural => "rural", } } pub fn next(self) -> PuebloTipo { let i = PuebloTipo::ALL.iter().position(|&t| t == self).unwrap_or(0); PuebloTipo::ALL[(i + 1) % PuebloTipo::ALL.len()] } /// Densidad urbana sugerida `[0,1]`: cuánto se agrupan las casas (cluster fbm). pub fn densidad(self) -> f32 { match self { PuebloTipo::Aldea => 0.25, PuebloTipo::Suburbio => 0.5, PuebloTipo::Ciudad => 0.7, PuebloTipo::Metropolis => 0.9, PuebloTipo::Prehistorico => 0.3, PuebloTipo::Costero => 0.4, PuebloTipo::Medieval => 0.55, PuebloTipo::Rural => 0.15, } } /// Mezcla de casas (estilo + probabilidad por parcela) característica del tipo. pub fn casas(self) -> Vec { let c = |tipo, densidad| CasaUso { tipo, densidad }; match self { PuebloTipo::Aldea => vec![ c(CasaTipo::Cabana, 0.4), c(CasaTipo::Adobe, 0.3), c(CasaTipo::Granja, 0.2), c(CasaTipo::Choza, 0.2), ], PuebloTipo::Suburbio => vec![ c(CasaTipo::Duplex, 0.45), c(CasaTipo::Moderna, 0.4), c(CasaTipo::Tienda, 0.15), c(CasaTipo::Granja, 0.1), ], PuebloTipo::Ciudad => vec![ c(CasaTipo::Edificio, 0.45), c(CasaTipo::Moderna, 0.3), c(CasaTipo::Tienda, 0.25), c(CasaTipo::Duplex, 0.2), ], PuebloTipo::Metropolis => vec![ c(CasaTipo::Rascacielos, 0.4), c(CasaTipo::Edificio, 0.5), c(CasaTipo::Moderna, 0.2), c(CasaTipo::Tienda, 0.2), ], PuebloTipo::Prehistorico => vec![ c(CasaTipo::Flintstone, 0.4), c(CasaTipo::Choza, 0.3), c(CasaTipo::Hobbit, 0.25), ], PuebloTipo::Costero => vec![ c(CasaTipo::Palafito, 0.4), c(CasaTipo::Choza, 0.3), c(CasaTipo::Cabana, 0.2), c(CasaTipo::Faro, 0.06), ], PuebloTipo::Medieval => vec![ c(CasaTipo::Adobe, 0.4), c(CasaTipo::Cabana, 0.3), c(CasaTipo::Torre, 0.15), c(CasaTipo::Granja, 0.2), ], PuebloTipo::Rural => vec![ c(CasaTipo::Granja, 0.4), c(CasaTipo::Cabana, 0.3), c(CasaTipo::Choza, 0.15), ], } } } /// Deriva una semilla de casa estable de una parcela de mundo `(cwx, cwz)` y la /// semilla del mundo → cada parcela genera una casa distinta pero determinista. pub fn casa_seed(world_seed: u32, cwx: i32, cwz: i32) -> u32 { world_seed .wrapping_add((cwx as u32).wrapping_mul(0x9E37_79B9)) .wrapping_add((cwz as u32).wrapping_mul(0x85EB_CA77)) ^ 0xCA5A } /// Hash entero determinista `[0, m)` a partir de `(seed, salt)`. #[inline] fn h(seed: u32, salt: u32, m: u32) -> u32 { let mut x = seed.wrapping_mul(0x9E37_79B9).wrapping_add(salt.wrapping_mul(0x85EB_CA77)); x ^= x >> 15; x = x.wrapping_mul(0x2C1B_3C6D); x ^= x >> 13; x % m.max(1) } /// Ruido `[0,1)` determinista para jitter fino. #[inline] fn hf(seed: u32, a: i32, b: i32) -> f32 { let mut x = seed .wrapping_add((a as u32).wrapping_mul(0x9E37_79B9)) .wrapping_add((b as u32).wrapping_mul(0x85EB_CA77)); x ^= x >> 15; x = x.wrapping_mul(0x2C1B_3C6D); x ^= x >> 12; (x & 0xFFFF) as f32 / 65536.0 } /// **Genera y estampa una casa** del `tipo` con la base (esquina/centro) en /// `(x, y, z)`, variada por `seed`. El piso arranca en `y`. pub fn stamp_casa(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, y: u32, z: u32, seed: u32) { let bx = x as i32; let by = y as i32; let bz = z as i32; let dim = g.dim(); // **Simétrica por construcción**: cada voxel va también a su espejo en `x` // (el frente mira a `+Z`, así que izquierda y derecha son `±dx`). Lo que un // estilo pone de un solo lado —la chimenea, el garaje, el tanque— sale de // los dos, que es como construye la gente; y lo que varía al azar por // columna queda igual a los dos lados porque el último que escribe escribe // en ambos. Ver `toda_casa_es_simetrica_izquierda_derecha`. let mut put = |dx: i32, dy: i32, dz: i32, mat: Material| { for dx in [dx, -dx] { let (px, py, pz) = (bx + dx, by + dy, bz + dz); if px >= 0 && py >= 0 && pz >= 0 && (px as u32) < dim[0] && (py as u32) < dim[1] && (pz as u32) < dim[2] { g.set_rgba(px as u32, py as u32, pz as u32, mat.color(), VoxelGrid::shine_to_alpha(mat.brillo())); } } }; match tipo { CasaTipo::Flintstone => { // Choza de piedra abovedada, con silueta lumpy y arco de entrada. let w = 4 + h(seed, 1, 3) as i32; // 4..6 (medio ancho) let d = 4 + h(seed, 2, 3) as i32; let base_h = 4 + h(seed, 3, 2) as i32; let piedra = [Material::Arenisca, Material::Grava, Material::Rock][h(seed, 9, 3) as usize]; for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if borde { // Altura de muro con bulto (piedra irregular). let bulto = (hf(seed, dx, dz) * 2.5) as i32; let alto = base_h + bulto; for dy in 0..alto { // Arco de puerta en el frente (dz = d). let puerta = dz == d && dx.abs() <= 1 && dy < 3; if !puerta { put(dx, dy, dz, piedra); } } } } } // Techo abovedado (cúpula chata de piedra/paja). let techo = if h(seed, 4, 2) == 0 { Material::Paja } else { piedra }; for dx in -w..=w { for dz in -d..=d { let r = (dx * dx) as f32 / (w * w) as f32 + (dz * dz) as f32 / (d * d) as f32; let cima = ((1.0 - r) * 3.0) as i32; if cima >= 0 { put(dx, base_h + cima, dz, techo); } } } } CasaTipo::Hobbit => { // Montículo de pasto con puerta y ventana redondas. let r = 5 + h(seed, 1, 3) as i32; // radio 5..7 let past = [Material::Grass, Material::Musgo, Material::Leaf][h(seed, 8, 3) as usize]; for dx in -r..=r { for dy in 0..=r { for dz in -r..=r { let dd = dx * dx + dy * dy + dz * dz; // Cáscara de la media esfera (montículo). if dd <= r * r && dd > (r - 2) * (r - 2) { put(dx, dy, dz, past); } } } } // Fachada + puerta redonda al frente (dz = r-1..r). let zf = r; for dx in -2..=2 { for dy in 0..4 { if dx * dx + (dy - 1) * (dy - 1) <= 4 { put(dx, dy, zf - 1, Material::Ladrillo); // marco de la puerta } } } for dx in -1..=1 { for dy in 0..3 { put(dx, dy, zf, Material::Madera); // puerta redonda de madera } } put(3, 2, zf - 1, Material::Vidrio); // ventanita redonda put(-3, 2, zf - 1, Material::Vidrio); // Chimenea. put(0, r + 1, 0, Material::Ladrillo); put(0, r + 2, 0, Material::Ladrillo); } CasaTipo::Cabana => { // Cabaña de troncos con techo a dos aguas y chimenea de piedra. let w = 3 + h(seed, 1, 3) as i32; // 3..5 let d = 4 + h(seed, 2, 3) as i32; // 4..6 let wall = 5; for dy in 0..wall { for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if borde { // Puerta al frente + ventanas. let puerta = dz == d && dx.abs() <= 1 && dy < 3; let ventana = (dx == -w || dx == w) && dy == 2 && dz.abs() == d / 2; let m = if puerta { continue; } else if ventana { Material::Vidrio } else if dy % 2 == 0 { Material::Madera // troncos (franjas) } else { Material::Adobe }; put(dx, dy, dz, m); } } } } // Techo a dos aguas (cumbrera a lo largo del eje más largo). let teja = if h(seed, 4, 2) == 0 { Material::Teja } else { Material::Pizarra }; let ridge_x = w >= d; for dx in -w - 1..=w + 1 { for dz in -d - 1..=d + 1 { let slope = if ridge_x { d + 1 - dz.abs() } else { w + 1 - dx.abs() }.max(0); put(dx, wall + slope, dz, teja); } } // Chimenea de piedra a un lado (varía). let cs = if h(seed, 5, 2) == 0 { -w } else { w }; for dy in 0..wall + d + 2 { put(cs, dy, -d + 1, Material::Rock); } } CasaTipo::Moderna => { // Casa moderna: hormigón + grandes ventanales, techo plano, 1–2 plantas. let w = 4 + h(seed, 1, 3) as i32; let d = 4 + h(seed, 2, 3) as i32; let plantas = 1 + h(seed, 3, 2) as i32; // 1..2 let ph = 5; // alto de planta let top = plantas * ph; for dy in 0..top { for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if borde { let piso_line = dy % ph == 0; // Ventanal: franjas de vidrio entre líneas de piso. let ventanal = !piso_line && (dy % ph) != ph - 1; let puerta = dz == d && dx.abs() <= 1 && dy < 3; if puerta { continue; } let m = if piso_line { Material::Hormigon } else if ventanal { Material::Vidrio } else { Material::Yeso }; put(dx, dy, dz, m); } } } } // Losa/techo plano. for dx in -w..=w { for dz in -d..=d { put(dx, top, dz, Material::Hormigon); } } // Parapeto + a veces un panel solar. if h(seed, 6, 2) == 0 { put(0, top + 1, 0, Material::Azul); put(1, top + 1, 0, Material::Azul); } } CasaTipo::Edificio => { // Bloque de departamentos: alto, con grilla de ventanas. let w = 3 + h(seed, 1, 3) as i32; let d = 3 + h(seed, 2, 2) as i32; let pisos = 5 + h(seed, 3, 5) as i32; // 5..9 let ph = 3; let top = pisos * ph; let muro = [Material::Ladrillo, Material::Hormigon, Material::Arcilla][h(seed, 7, 3) as usize]; for dy in 0..top { for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if borde { // Ventana en cada piso (mitad de la altura), columnas alternas. let ventana = dy > 0 && dy % ph == 1 && (dx + dz).rem_euclid(2) == 0; let puerta = dz == d && dx.abs() <= 1 && dy < 2; if puerta { continue; } put(dx, dy, dz, if ventana { Material::Vidrio } else { muro }); } } } } for dx in -w..=w { for dz in -d..=d { put(dx, top, dz, Material::Hormigon); // azotea } } // Tanque/antena. put(-w + 1, top + 1, -d + 1, Material::Acero); } CasaTipo::Iglu => { let r = 4 + h(seed, 1, 2) as i32; // 4..5 for dx in -r..=r { for dy in 0..=r { for dz in -r..=r { let dd = dx * dx + dy * dy + dz * dz; if dd <= r * r && dd > (r - 2) * (r - 2) { put(dx, dy, dz, Material::Snow); // cúpula de nieve } } } } for dz in r..=r + 2 { for dx in -1..=1 { for dy in 0..3 { if dx * dx + (dy - 1) * (dy - 1) <= 2 { put(dx, dy, dz, Material::Hielo); // túnel } } } } } CasaTipo::Palafito => { let w = 3 + h(seed, 1, 2) as i32; let d = 3 + h(seed, 2, 2) as i32; let alto = 4; for &(cx, cz) in &[(-w, -d), (w, -d), (-w, d), (w, d)] { for dy in 0..alto { put(cx, dy, cz, Material::Madera); // pilotes } } for dx in -w..=w { for dz in -d..=d { put(dx, alto, dz, Material::Madera); // plataforma } } for dy in (alto + 1)..(alto + 5) { for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if borde { if dz == d && dx.abs() <= 1 && dy < alto + 3 { continue; // puerta } let ventana = (dx == -w || dx == w) && dy == alto + 2; put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Madera }); } } } } for dx in (-w - 1)..=(w + 1) { for dz in (-d - 1)..=(d + 1) { let slope = (d + 1 - dz.abs()).max(0); put(dx, alto + 5 + slope, dz, Material::Paja); } } for dy in 0..alto { put(0, dy, d + 1, Material::Madera); // escalera } } CasaTipo::Adobe => { let w = 3 + h(seed, 1, 3) as i32; let d = 3 + h(seed, 2, 3) as i32; let h1 = 4; for dy in 0..h1 { for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if borde { if dz == d && dx.abs() <= 1 && dy < 3 { continue; // puerta } let ventana = dy == 2 && dx.abs() == w && dz.abs() == d / 2; put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Adobe }); } } } } for dx in -w..=w { for dz in -d..=d { put(dx, h1, dz, Material::Adobe); // techo plano } } let mut vx = -w; while vx <= w { put(vx, h1 - 1, d + 1, Material::Madera); // vigas que sobresalen put(vx, h1 - 1, -d - 1, Material::Madera); vx += 2; } let ow = (w - 1).max(1); for dy in h1..(h1 + 3) { for dx in -ow..=(ow - 1) { for dz in -ow..=(ow - 1) { let borde = dx == -ow || dx == ow - 1 || dz == -ow || dz == ow - 1; if borde { put(dx, dy, dz, Material::Arcilla); // segundo cuerpo apilado } } } } for dx in -ow..=(ow - 1) { for dz in -ow..=(ow - 1) { put(dx, h1 + 3, dz, Material::Adobe); } } } CasaTipo::Rascacielos => { let ht = 14 + h(seed, 1, 12) as i32; // 14..25 let w = 2 + h(seed, 2, 2) as i32; // 2..3 for dy in 0..ht { for dx in -w..=w { for dz in -w..=w { let borde = dx == -w || dx == w || dz == -w || dz == w; if borde { let esquina = dx.abs() == w && dz.abs() == w; let banda = dy % 4 == 0; put(dx, dy, dz, if esquina || banda { Material::Acero } else { Material::Vidrio }); } } } } for dx in -w..=w { for dz in -w..=w { put(dx, ht, dz, Material::Hormigon); } } put(0, ht + 1, 0, Material::Acero); put(0, ht + 2, 0, Material::Acero); } CasaTipo::Granja => { let w = 3i32; let d = 3i32; for dy in 0..5 { for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if borde { if dz == d && dx.abs() <= 1 && dy < 3 { continue; // puerta } let ventana = dy == 2 && dx.abs() == w && dz == 0; put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Yeso }); } } } } for dx in (-w - 1)..=(w + 1) { for dz in (-d - 1)..=(d + 1) { let slope = (d + 1 - dz.abs()).max(0); put(dx, 5 + slope, dz, Material::Teja); } } for dy in 0..7 { put(w, dy, -d + 1, Material::Ladrillo); // chimenea } // Granero al costado. let cx = w + 5; for dy in 0..6 { for dx in (cx - 3)..=(cx + 3) { for dz in -3..=3 { let borde = dx == cx - 3 || dx == cx + 3 || dz == -3 || dz == 3; if borde { if dz == 3 && (dx - cx).abs() <= 1 && dy < 4 { continue; // portón } put(dx, dy, dz, Material::Ladrillo); } } } } for dx in (cx - 4)..=(cx + 4) { for dz in -4i32..=4 { let slope = (4 - dz.abs()).max(0); put(dx, 6 + slope, dz, Material::Teja); } } } CasaTipo::Torre => { // Torreón de piedra: planta cuadrada, alto variable, almenas arriba. let r = 2 + h(seed, 1, 2) as i32; // 2..3 let alto = 8 + h(seed, 2, 6) as i32; // 8..13 let piedra = [Material::Granito, Material::Arenisca, Material::Basalto][h(seed, 7, 3) as usize]; for dy in 0..alto { for dx in -r..=r { for dz in -r..=r { let borde = dx == -r || dx == r || dz == -r || dz == r; if borde { if dz == r && dx == 0 && dy < 3 { continue; // portón } // Aspilleras: ranuras verticales angostas. let aspillera = dy > 2 && dy % 3 == 0 && (dx == 0 || dz == 0); put(dx, dy, dz, if aspillera { Material::Vidrio } else { piedra }); } } } } // Almenas: dientes alternados en el borde superior. for dx in -r..=r { for dz in -r..=r { let borde = dx == -r || dx == r || dz == -r || dz == r; if borde && (dx + dz).rem_euclid(2) == 0 { put(dx, alto, dz, piedra); } } } } CasaTipo::Faro => { // Faro: torre cilíndrica con franjas blancas y rojas + linterna acristalada. let alto = 12 + h(seed, 1, 6) as i32; // 12..17 let r = 3; for dy in 0..alto { let banda = (dy / 3) % 2 == 0; let piel = if banda { Material::Yeso } else { Material::Ladrillo }; for dx in -r..=r { for dz in -r..=r { // Aproximación de círculo por radio. let dist2 = dx * dx + dz * dz; let anillo = dist2 <= r * r && dist2 > (r - 1) * (r - 1); if anillo { if dz == r - 0 && dx == 0 && dy < 3 { continue; // puerta } put(dx, dy, dz, piel); } } } } // Galería + linterna de vidrio. for dx in -r..=r { for dz in -r..=r { if dx * dx + dz * dz <= r * r { put(dx, alto, dz, Material::Acero); } } } for dy in (alto + 1)..(alto + 4) { for dx in -(r - 1)..=(r - 1) { for dz in -(r - 1)..=(r - 1) { let dist2 = dx * dx + dz * dz; if dist2 <= (r - 1) * (r - 1) && dist2 > (r - 2).max(0) * (r - 2).max(0) { put(dx, dy, dz, Material::Vidrio); } } } } put(0, alto + 4, 0, Material::Bloom); // remate de la linterna } CasaTipo::Choza => { // Choza tropical: paredes bajas de adobe/madera + techo cónico de paja. let r = 2 + h(seed, 1, 2) as i32; // 2..3 let pared = if h(seed, 5, 2) == 0 { Material::Adobe } else { Material::Madera }; for dy in 0..3 { for dx in -r..=r { for dz in -r..=r { let borde = dx == -r || dx == r || dz == -r || dz == r; if borde { if dz == r && dx == 0 && dy < 2 { continue; // vano } put(dx, dy, dz, pared); } } } } // Techo cónico de paja: capas que se cierran hacia la punta. let mut rr = r + 1; let mut dy = 3; while rr >= 0 { for dx in -rr..=rr { for dz in -rr..=rr { if dx.abs() == rr || dz.abs() == rr || rr == 0 { put(dx, dy, dz, Material::Paja); } } } rr -= 1; dy += 1; } } CasaTipo::Tienda => { // Local a pie de calle: caja baja, vidriera al frente, toldo y cartel. let w = 4 + h(seed, 1, 3) as i32; let d = 3 + h(seed, 2, 2) as i32; let alto = 4; let muro = [Material::Ladrillo, Material::Yeso, Material::Arcilla][h(seed, 7, 3) as usize]; for dy in 0..alto { for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if borde { // Frente (dz == d): vidriera grande + puerta. if dz == d && dy < 3 { if dx.abs() <= w - 1 { put(dx, dy, dz, Material::Vidrio); } continue; } put(dx, dy, dz, muro); } } } } // Techo plano + cartel. for dx in -w..=w { for dz in -d..=d { put(dx, alto, dz, Material::Hormigon); } } // Toldo: alero rayado sobre la vidriera. let toldo = [Material::Ladrillo, Material::Bloom, Material::Azul][h(seed, 8, 3) as usize]; for dx in -w..=w { put(dx, 3, d + 1, if dx.rem_euclid(2) == 0 { toldo } else { Material::Yeso }); } // Cartel sobre el toldo. for dx in -(w - 1)..=(w - 1) { put(dx, alto + 1, d, muro); } } CasaTipo::Duplex => { // Dúplex suburbano: dos plantas, techo a dos aguas, garaje anexo. let w = 4 + h(seed, 1, 2) as i32; let d = 4 + h(seed, 2, 2) as i32; let alto = 8; // dos plantas de 4 let muro = [Material::Yeso, Material::Ladrillo, Material::Arcilla][h(seed, 7, 3) as usize]; for dy in 0..alto { for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if borde { let puerta = dz == d && dx.abs() <= 1 && dy < 3; if puerta { continue; } // Ventanas: una por planta a media altura. let ventana = (dy == 2 || dy == 6) && (dx.abs() == 2 || dz.abs() == 2); put(dx, dy, dz, if ventana { Material::Vidrio } else { muro }); } } } } // Techo a dos aguas de teja. for dx in (-w - 1)..=(w + 1) { for dz in (-d - 1)..=(d + 1) { let slope = (d + 1 - dz.abs()).max(0); put(dx, alto + slope, dz, Material::Teja); } } // Garaje anexo bajo. let gx = w + 4; for dy in 0..4 { for dx in (gx - 3)..=(gx + 3) { for dz in -3..=3 { let borde = dx == gx - 3 || dx == gx + 3 || dz == -3 || dz == 3; if borde { if dz == 3 && (dx - gx).abs() <= 2 && dy < 3 { continue; // portón del garaje } put(dx, dy, dz, muro); } } } } for dx in (gx - 3)..=(gx + 3) { for dz in -3..=3 { put(dx, 4, dz, Material::Hormigon); } } } CasaTipo::Palacio => palacio(g, (bx, by, bz), seed), CasaTipo::Kiosko => kiosko(g, (bx, by, bz), seed), } } /// Un color con su brillo, de un material o de un mineral con nombre. type Tinte = ([u8; 3], f32); fn de_mineral(m: Mineral) -> Tinte { (m.color(), m.brillo()) } fn de_material(m: Material) -> Tinte { (m.color(), m.brillo()) } /// Pone un voxel relativo a la base, si cae en la grilla — y su espejo en `x`, /// por la misma regla que el `put` de [`stamp_casa`]: toda construcción es /// simétrica izquierda-derecha. fn pon(g: &mut VoxelGrid, base: (i32, i32, i32), dx: i32, dy: i32, dz: i32, t: Tinte) { let dim = g.dim(); for dx in [dx, -dx] { let (px, py, pz) = (base.0 + dx, base.1 + dy, base.2 + dz); if px >= 0 && py >= 0 && pz >= 0 && (px as u32) < dim[0] && (py as u32) < dim[1] && (pz as u32) < dim[2] { g.set_rgba(px as u32, py as u32, pz as u32, t.0, VoxelGrid::shine_to_alpha(t.1)); } } } /// El palacio: escalinata de mármol, cuerpo de mármol con ventanales de /// diamante y portal de oro, cuatro torres con aguja de diamante, cúpula de /// diamante y remate de oro. El frente mira a `+Z`, como las demás. fn palacio(g: &mut VoxelGrid, b: (i32, i32, i32), seed: u32) { let marmol = de_material(Material::Marmol); let diamante = de_mineral(Mineral::Diamante); let oro = de_mineral(Mineral::Oro); // Grande: el rey y la princesa miden unos cuatro voxels, y un palacio que // les llega a la cintura de las torres se lee como una casa con agujas. let w = 15 + h(seed, 1, 2) as i32; // medio ancho del cuerpo: 15..16 let d = 12; let alto = 12 + h(seed, 2, 2) as i32; // 12..13 // Escalinata: dos gradas de mármol más anchas que el cuerpo, la de abajo // asoma al frente. for (dy, extra, frente) in [(0, 3, 4), (1, 2, 2)] { for dx in -(w + extra)..=(w + extra) { for dz in -(d + extra)..=(d + frente) { pon(g, b, dx, dy, dz, marmol); } } } let y0 = 2; // Cuerpo: muros de mármol; ventanales de diamante a media altura; portal // de tres de ancho con marco de oro. for dy in 0..alto { for dx in -w..=w { for dz in -d..=d { let borde = dx == -w || dx == w || dz == -d || dz == d; if !borde { continue; } let portal = dz == d && dx.abs() <= 2 && dy < 6; if portal { continue; } let marco = dz == d && dx.abs() == 3 && dy < 7 || dz == d && dx.abs() <= 3 && dy == 6; // Dos hileras de ventanales: la planta baja y la alta. let fila = (3..=5).contains(&dy) || (8..=10).contains(&dy); let ventana = fila && (dx.rem_euclid(3) == 0 || dz.rem_euclid(3) == 0) && !(dx.abs() == w && dz.abs() == d); let t = if marco { oro } else if ventana { diamante } else { marmol }; pon(g, b, dx, y0 + dy, dz, t); } } } // Techo plano de mármol con cornisa de oro. for dx in -(w + 1)..=(w + 1) { for dz in -(d + 1)..=(d + 1) { let cornisa = dx.abs() == w + 1 || dz.abs() == d + 1; pon(g, b, dx, y0 + alto, dz, if cornisa { oro } else { marmol }); } } // Cuatro torres en las esquinas, más altas que el cuerpo, con aguja de diamante. let r = 3; let alto_torre = alto + 8; for (sx, sz) in [(-1, -1), (1, -1), (-1, 1), (1, 1)] { let (cx, cz) = (sx * w, sz * d); for dy in 0..alto_torre { for dx in -r..=r { for dz in -r..=r { if dx * dx + dz * dz <= r * r + 1 { let t = if dy == alto_torre - 1 { oro } else { marmol }; pon(g, b, cx + dx, y0 + dy, cz + dz, t); } } } } // Aguja: cono de diamante que se afina en seis escalones. for k in 0..6 { let rr = (r - k / 2).max(0); for dx in -rr..=rr { for dz in -rr..=rr { if dx * dx + dz * dz <= rr * rr { pon(g, b, cx + dx, y0 + alto_torre + k, cz + dz, diamante); } } } } pon(g, b, cx, y0 + alto_torre + 6, cz, oro); } // Cúpula central de diamante sobre el techo, con remate de oro. let rc = 8; let yc = y0 + alto + 1; for dx in -rc..=rc { for dy in 0..=rc { for dz in -rc..=rc { let dd = dx * dx + dy * dy + dz * dz; if dd <= rc * rc && dd > (rc - 1) * (rc - 1) { pon(g, b, dx, yc + dy, dz, diamante); } } } } pon(g, b, 0, yc + rc + 1, 0, oro); pon(g, b, 0, yc + rc + 2, 0, oro); } /// El kiosko: piso de mármol, ocho columnas y un domo bajo de malaquita (franjas /// de cobre y jade, que es lo que la malaquita es), con borde y remate de oro. /// Abierto: se ve a través y se puede estar adentro. fn kiosko(g: &mut VoxelGrid, b: (i32, i32, i32), seed: u32) { let marmol = de_material(Material::Marmol); let oro = de_mineral(Mineral::Oro); let malaquita = |dy: i32| de_mineral(if dy.rem_euclid(2) == 0 { Mineral::Cobre } else { Mineral::Jade }); let r = 6 + h(seed, 1, 2) as i32; // 6..7 // Piso redondo de mármol, una grada. for dx in -(r + 1)..=(r + 1) { for dz in -(r + 1)..=(r + 1) { if dx * dx + dz * dz <= (r + 1) * (r + 1) { pon(g, b, dx, 0, dz, marmol); } } } // Ocho columnas en el borde. let alto = 6; for i in 0..8 { let a = i as f32 * std::f32::consts::TAU / 8.0; let (cx, cz) = ((a.cos() * r as f32).round() as i32, (a.sin() * r as f32).round() as i32); for dy in 1..=alto { pon(g, b, cx, dy, cz, malaquita(dy)); } } // Aro de oro arriba de las columnas. for dx in -r..=r { for dz in -r..=r { let dd = dx * dx + dz * dz; if dd <= r * r && dd > (r - 1) * (r - 1) { pon(g, b, dx, alto + 1, dz, oro); } } } // Domo bajo (media esfera achatada) de malaquita en franjas. for dx in -r..=r { for dy in 0..=r { for dz in -r..=r { let dd = dx * dx + (dy * 2) * (dy * 2) + dz * dz; if dd <= r * r && dd > (r - 1) * (r - 1) { pon(g, b, dx, alto + 2 + dy, dz, malaquita(dy)); } } } } let cima = alto + 2 + r / 2 + 1; pon(g, b, 0, cima, 0, oro); pon(g, b, 0, cima + 1, 0, oro); } /// **Planta una casa a mano** en la columna `(x, z)` de la grilla: busca el /// suelo, despeja lo que haya encima en su [`huella`](CasaTipo::huella) (árboles, /// pasto, otra casa) y rellena lo que falte abajo, así la casa queda apoyada en /// un piso parejo, y después la estampa. Es lo que usa un decorado de escena: /// la generación procedural nivela su propio casco urbano, pero una casa /// puesta en un punto cualquiera cae sobre el relieve que haya. /// /// Devuelve la `y` del piso, o `None` si la columna no tiene suelo. pub fn plantar_casa(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, z: u32, seed: u32) -> Option { plantar_casa_girada(g, tipo, x, z, seed, 0) } /// Igual que [`plantar_casa`], con el frente girado `giro` cuartos de vuelta /// alrededor de su centro (`0` = a `+Z`, `1` = a `+X`, `2` = a `-Z`, `3` = a /// `-X`). La casa se genera en una grilla aparte y se copia rotada: el /// generador de cada estilo sigue escribiendo «de frente a +Z» sin saber nada. pub fn plantar_casa_girada(g: &mut VoxelGrid, tipo: CasaTipo, x: u32, z: u32, seed: u32, giro: u8) -> Option { let dim = g.dim(); if x >= dim[0] || z >= dim[2] { return None; } let suelo = g.height_at(x, z)?; let piso = suelo + 1; let r = tipo.huella(); for dx in -r..=r { for dz in -r..=r { if dx * dx + dz * dz > r * r { continue; } let (px, pz) = (x as i32 + dx, z as i32 + dz); if px < 0 || pz < 0 || px as u32 >= dim[0] || pz as u32 >= dim[2] { continue; } let (px, pz) = (px as u32, pz as u32); // El material del suelo de esa columna, para rellenar con lo mismo. let tope = g.height_at(px, pz); let relleno = tope.and_then(|y| g.get(px, y, pz)).unwrap_or([120, 110, 100, 255]); for y in piso..dim[1] { g.clear(px, y, pz); } let desde = tope.map(|y| y + 1).unwrap_or(0); for y in desde..piso { g.set_rgba(px, y, pz, [relleno[0], relleno[1], relleno[2]], relleno[3]); } } } let giro = giro % 4; if giro == 0 { stamp_casa(g, tipo, x, piso, z, seed); return Some(piso); } // Una grilla de la huella entera (más un margen: algún estilo asoma un // voxel de su huella), con el centro en el medio. let m = r + 4; let lado = (2 * m + 1) as u32; let mut aparte = VoxelGrid::new([lado, dim[1], lado]); stamp_casa(&mut aparte, tipo, m as u32, piso, m as u32, seed); for ax in 0..lado { for az in 0..lado { let (dx, dz) = (ax as i32 - m, az as i32 - m); // `+Z` gira a `+X` con un cuarto de vuelta: (dx, dz) → (dz, -dx). let (rx, rz) = match giro { 1 => (dz, -dx), 2 => (-dx, -dz), _ => (-dz, dx), }; let (px, pz) = (x as i32 + rx, z as i32 + rz); if px < 0 || pz < 0 || px as u32 >= dim[0] || pz as u32 >= dim[2] { continue; } for y in 0..dim[1] { if let Some(c) = aparte.get(ax, y, az).filter(|c| c[3] > 0) { g.set_rgba(px as u32, y, pz as u32, [c[0], c[1], c[2]], c[3]); } } } } Some(piso) } #[cfg(test)] mod tests { use super::*; fn solids(g: &VoxelGrid, dim: [u32; 3]) -> usize { let mut n = 0; for x in 0..dim[0] { for y in 0..dim[1] { for z in 0..dim[2] { if g.is_solid(x as i32, y as i32, z as i32) { n += 1; } } } } n } #[test] fn cada_tipo_genera_una_casa() { let dim = [40u32, 40, 40]; for tipo in CasaTipo::ALL { let mut g = VoxelGrid::new(dim); stamp_casa(&mut g, tipo, 20, 1, 20, 7); assert!(solids(&g, dim) >= 30, "{} generó muy poco", tipo.label()); } } #[test] fn misma_semilla_misma_casa_distinta_semilla_distinta() { let dim = [40u32, 40, 40]; let build = |seed| { let mut g = VoxelGrid::new(dim); stamp_casa(&mut g, CasaTipo::Cabana, 20, 1, 20, seed); solids(&g, dim) }; assert_eq!(build(3), build(3), "misma semilla → misma casa"); // Con muchas semillas, al menos algunas dan tamaños distintos. let n: std::collections::BTreeSet = (0..8).map(build).collect(); assert!(n.len() > 1, "distintas semillas dan casas distintas"); } /// **Toda construcción es simétrica**: el lado izquierdo igual que el /// derecho, visto de frente (el frente mira a `+Z`, así que el espejo es en /// `x`). Así construyen los humanos, y el operador lo puso como regla sin /// excepción: una casa torcida se lee como un error del generador, no como /// un estilo. Se mide voxel por voxel, color incluido, y con varias semillas /// porque la asimetría suele venir de una variación al azar. #[test] fn toda_casa_es_simetrica_izquierda_derecha() { let dim = [72u32, 96, 72]; let cx = 36u32; let mut malas = Vec::new(); for tipo in CasaTipo::ALL { for seed in 0..6 { let mut g = VoxelGrid::new(dim); stamp_casa(&mut g, tipo, cx, 1, 36, seed); let mut distintos = 0; for x in 0..dim[0] { let espejo = 2 * cx as i64 - x as i64; if !(0..dim[0] as i64).contains(&espejo) { continue; } for y in 0..dim[1] { for z in 0..dim[2] { let a = g.get(x, y, z).filter(|c| c[3] > 0); let b = g.get(espejo as u32, y, z).filter(|c| c[3] > 0); if a != b { distintos += 1; } } } } if distintos > 0 { malas.push(format!("{} (semilla {seed}): {distintos} voxels sin espejo", tipo.label())); } } } assert!(malas.is_empty(), "construcciones asimétricas:\n{}", malas.join("\n")); } #[test] fn los_presets_de_pueblo_traen_casas_y_densidad() { for pt in PuebloTipo::ALL { let casas = pt.casas(); assert!(!casas.is_empty(), "{} sin casas", pt.label()); assert!(casas.iter().all(|c| c.densidad > 0.0), "{} con densidad 0", pt.label()); let d = pt.densidad(); assert!((0.05..=0.95).contains(&d), "{} densidad fuera de rango: {d}", pt.label()); } // La metrópolis es más densa que la aldea. assert!(PuebloTipo::Metropolis.densidad() > PuebloTipo::Aldea.densidad()); } }