tawasuyu

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Sutichaykuna

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 llimphi-voxel/src/worldgen.rs Rust · 1696 siq'ikuna · 70.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

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//! **Bioma**: el relieve + qué materiales lo pintan. Antes esto era `WorldRecipe`
2//! con los materiales clavados en un `enum`; ahora un [`Bioma`] referencia
3//! **materiales autorables por id** (los define el [`Project`](crate::Project)) y la
4//! semilla vive en el [`Mundo`](crate::Mundo) que lo envuelve. El render no consume
5//! ids: la app **resuelve** el bioma a una [`BiomaPalette`] (colores concretos) y la
6//! pasa a [`Bioma::generate_window`].
7//!
8//! La altura sigue siendo **función pura de mundo** ([`Bioma::column_height`]):
9//! mismo `(seed, wx, wz)` → misma columna, así un bioma también streamea
10//! (continuidad de [`WorldStream`](crate::WorldStream)).
11//!
12//! El `enum` [`Material`] sobrevive **sólo como paleta semilla**: da los colores
13//! de fábrica (arena, pasto, roca…) con los que el `Project` siembra sus
14//! `MaterialDef` iniciales. No es el material autorable — ese es
15//! [`MaterialDef`](crate::MaterialDef).
16
17use llimphi_3d::VoxelGrid;
18use serde::{Deserialize, Serialize};
19
20use crate::mineral::{aspecto_hit, mineral_shade, MineralUso};
21use crate::terrain::{fbm, hash2, smooth, world_scale};
22
23/// **Paleta semilla**: los materiales de fábrica con su color canónico. El
24/// `Project` siembra un `MaterialDef` por variante (salvo `Air`) al arrancar, y la
25/// UI puede ofrecerlos como punto de partida. No es el material autorable.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub enum Material {
28    /// Vacío (aire) — no se dibuja.
29    Air,
30    /// Arena del desierto.
31    Sand,
32    /// Tierra/pasto (praderas).
33    Grass,
34    /// Roca de acantilado / montaña.
35    Rock,
36    /// Nieve de cumbre.
37    Snow,
38    /// Agua (superficie de ríos/lagos/mar).
39    Water,
40    /// Carne de cactus (verde).
41    Cactus,
42    /// Follaje/hojas (copas de árboles, arbustos, palmeras) — verde profundo.
43    Leaf,
44    /// Grano dorado (espigas de trigo, pastos secos).
45    Wheat,
46    /// Corola/flor (pétalos coloridos) — violeta lavanda.
47    Bloom,
48    /// Tierra parda (suelo desnudo).
49    Tierra,
50    /// Arcilla rojiza.
51    Arcilla,
52    /// Grava gris suelta.
53    Grava,
54    /// Arenisca (roca sedimentaria clara).
55    Arenisca,
56    /// Basalto (roca volcánica oscura).
57    Basalto,
58    /// Granito (roca ígnea moteada).
59    Granito,
60    /// Mármol (roca clara pulida).
61    Marmol,
62    /// Hielo (glaciar, congelado).
63    Hielo,
64    /// Lava (roca fundida, brillante).
65    Lava,
66    /// Musgo (verde húmedo sobre roca/tierra).
67    Musgo,
68    // --- Materiales de casa (construcción) ---
69    /// Ladrillo cocido (muros).
70    Ladrillo,
71    /// Madera de tablón (vigas, pisos, puertas).
72    Madera,
73    /// Hormigón (concreto gris).
74    Hormigon,
75    /// Yeso/estuco (revoque claro).
76    Yeso,
77    /// Teja de barro (techos).
78    Teja,
79    /// Adobe (barro secado al sol).
80    Adobe,
81    /// Pizarra (techo oscuro).
82    Pizarra,
83    /// Paja (techo de paja).
84    Paja,
85    /// Vidrio (ventanas) — lustroso.
86    Vidrio,
87    /// Acero (estructura metálica) — lustroso.
88    Acero,
89    /// Cerámica esmaltada (azulejos) — lustrosa.
90    Ceramica,
91    /// Azul profundo brillante (paneles solares, vidrio azulado, revestimientos).
92    Azul,
93}
94
95impl Material {
96    /// Color RGB de fábrica del material.
97    pub fn color(self) -> [u8; 3] {
98        match self {
99            Material::Air => [0, 0, 0],
100            Material::Sand => [214, 188, 130],
101            Material::Grass => [84, 140, 64],
102            Material::Rock => [108, 102, 98],
103            Material::Snow => [236, 240, 250],
104            Material::Water => [54, 118, 158],
105            Material::Cactus => [74, 128, 70],
106            Material::Leaf => [56, 108, 52],
107            Material::Wheat => [214, 182, 96],
108            Material::Bloom => [158, 116, 202],
109            Material::Tierra => [110, 82, 54],
110            Material::Arcilla => [172, 120, 92],
111            Material::Grava => [140, 138, 132],
112            Material::Arenisca => [206, 172, 122],
113            Material::Basalto => [58, 56, 64],
114            Material::Granito => [156, 142, 140],
115            Material::Marmol => [232, 230, 226],
116            Material::Hielo => [198, 224, 236],
117            Material::Lava => [232, 100, 42],
118            Material::Musgo => [78, 108, 58],
119            Material::Ladrillo => [168, 76, 58],
120            Material::Madera => [150, 104, 60],
121            Material::Hormigon => [176, 174, 168],
122            Material::Yeso => [224, 218, 206],
123            Material::Teja => [178, 88, 60],
124            Material::Adobe => [198, 156, 110],
125            Material::Pizarra => [72, 76, 84],
126            Material::Paja => [196, 168, 96],
127            Material::Vidrio => [176, 208, 220],
128            Material::Acero => [150, 154, 162],
129            Material::Ceramica => [96, 168, 190],
130            Material::Azul => [40, 66, 150],
131        }
132    }
133
134    /// **Grano de materia** de fábrica `[0,1]` (textura por vóxel; ver [`grained`]).
135    pub fn grain(self) -> f32 {
136        match self {
137            Material::Sand => 0.55,
138            Material::Grass | Material::Rock => 0.45,
139            Material::Leaf => 0.5,
140            Material::Wheat => 0.5,
141            Material::Snow => 0.25,
142            Material::Tierra | Material::Grava | Material::Musgo => 0.55,
143            Material::Arcilla | Material::Arenisca => 0.45,
144            Material::Basalto | Material::Granito => 0.4,
145            Material::Marmol => 0.15,
146            Material::Hielo => 0.12,
147            Material::Lava => 0.2,
148            Material::Ladrillo | Material::Adobe => 0.4,
149            Material::Madera => 0.45,
150            Material::Hormigon => 0.3,
151            Material::Yeso => 0.2,
152            Material::Teja => 0.35,
153            Material::Pizarra => 0.25,
154            Material::Paja => 0.5,
155            Material::Vidrio => 0.05,
156            Material::Acero | Material::Ceramica => 0.1,
157            Material::Azul => 0.12,
158            Material::Air | Material::Water | Material::Cactus | Material::Bloom => 0.0,
159        }
160    }
161
162    /// **Brillo/reflectividad** `[0,1]` de fábrica del material: `0` = mate. Los de
163    /// obra lustrosos (vidrio, acero, cerámica esmaltada) y las rocas pulidas
164    /// (mármol) o vidriosas (hielo) devuelven lustre especular al sol.
165    pub fn brillo(self) -> f32 {
166        match self {
167            Material::Vidrio => 0.85,
168            Material::Acero => 0.7,
169            Material::Hielo => 0.62,
170            Material::Ceramica => 0.58,
171            Material::Azul => 0.45,
172            Material::Marmol => 0.5,
173            Material::Lava => 0.28,
174            _ => 0.0,
175        }
176    }
177
178    /// `true` salvo para [`Material::Air`].
179    pub fn is_solid(self) -> bool {
180        !matches!(self, Material::Air)
181    }
182
183    /// Las variantes sembrables (sin `Air`), en orden de catálogo.
184    pub const ALL: [Material; 31] = [
185        Material::Sand,
186        Material::Grass,
187        Material::Rock,
188        Material::Snow,
189        Material::Water,
190        Material::Cactus,
191        Material::Leaf,
192        Material::Wheat,
193        Material::Bloom,
194        Material::Tierra,
195        Material::Arcilla,
196        Material::Grava,
197        Material::Arenisca,
198        Material::Basalto,
199        Material::Granito,
200        Material::Marmol,
201        Material::Hielo,
202        Material::Lava,
203        Material::Musgo,
204        Material::Ladrillo,
205        Material::Madera,
206        Material::Hormigon,
207        Material::Yeso,
208        Material::Teja,
209        Material::Adobe,
210        Material::Pizarra,
211        Material::Paja,
212        Material::Vidrio,
213        Material::Acero,
214        Material::Ceramica,
215        Material::Azul,
216    ];
217
218    /// Nombre legible (español).
219    pub fn label(self) -> &'static str {
220        match self {
221            Material::Air => "aire",
222            Material::Sand => "arena",
223            Material::Grass => "pasto",
224            Material::Rock => "roca",
225            Material::Snow => "nieve",
226            Material::Water => "agua",
227            Material::Cactus => "cactus",
228            Material::Leaf => "follaje",
229            Material::Wheat => "trigo",
230            Material::Bloom => "flor",
231            Material::Tierra => "tierra",
232            Material::Arcilla => "arcilla",
233            Material::Grava => "grava",
234            Material::Arenisca => "arenisca",
235            Material::Basalto => "basalto",
236            Material::Granito => "granito",
237            Material::Marmol => "mármol",
238            Material::Hielo => "hielo",
239            Material::Lava => "lava",
240            Material::Musgo => "musgo",
241            Material::Ladrillo => "ladrillo",
242            Material::Madera => "madera",
243            Material::Hormigon => "hormigón",
244            Material::Yeso => "yeso",
245            Material::Teja => "teja",
246            Material::Adobe => "adobe",
247            Material::Pizarra => "pizarra",
248            Material::Paja => "paja",
249            Material::Vidrio => "vidrio",
250            Material::Acero => "acero",
251            Material::Ceramica => "cerámica",
252            Material::Azul => "azul",
253        }
254    }
255
256    /// El material siguiente del catálogo (cicla, salta Air).
257    pub fn next(self) -> Material {
258        let i = Material::ALL.iter().position(|&m| m == self).unwrap_or(0);
259        Material::ALL[(i + 1) % Material::ALL.len()]
260    }
261}
262
263/// **Forma de un objeto** colocable (un material con rol objeto): el catálogo de
264/// vegetación que brota sobre el terreno. Cada variante es una silueta de vóxels
265/// distinta que [`Bioma::place_objeto`] estampa de forma determinista por posición
266/// de mundo (seamless entre ventanas de streaming). El **color de follaje** lo pone
267/// el material del objeto; los troncos leñosos usan un marrón interno ([`WOOD`]).
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
269pub enum Forma {
270    /// Columna vertical con brazos (cactus).
271    Columnar,
272    /// Matas de grama: unas pocas hojas cortas (1–2 de alto).
273    Pasto,
274    /// Arbusto: mata redondeada de follaje baja, sin tronco.
275    Arbusto,
276    /// Árbol frondoso: tronco leñoso + copa esférica.
277    Arbol,
278    /// Conífera: tronco corto + tiers cónicos apilados que rematan en punta.
279    Conifera,
280    /// Palmera: tronco alto y fino (con leve curva) + corona de frondas radiales.
281    Palmera,
282    /// Helecho: frondas bajas que arquean desde la base, sin tronco.
283    Helecho,
284    /// Flor: tallo fino verde + corola coloreada en cruz.
285    Flor,
286    /// Bambú (selva): mata de cañas altas y finas con nudos.
287    Bambu,
288    /// Platanera (selva): tallo corto + hojas anchas grandes que arquean.
289    Platanera,
290    /// Roble (bosque): tronco grueso + copa amplia achatada.
291    Roble,
292    /// Hongo (bosque): pie pálido + sombrero domado (color propio).
293    Hongo,
294    /// Tocón (bosque): cepa gruesa y baja de madera con musgo arriba.
295    Tocon,
296    /// Trigo (pradera): manojo de espigas altas doradas.
297    Trigo,
298    /// Junco (pradera/humedal): cañas finas altas con espadaña marrón.
299    Junco,
300    /// Lavanda (pradera): matojo bajo con espigas florales coloreadas.
301    Lavanda,
302}
303
304impl Forma {
305    /// Todas las formas (para ciclar en un editor).
306    pub const ALL: [Forma; 16] = [
307        Forma::Columnar,
308        Forma::Pasto,
309        Forma::Arbusto,
310        Forma::Arbol,
311        Forma::Conifera,
312        Forma::Palmera,
313        Forma::Helecho,
314        Forma::Flor,
315        Forma::Bambu,
316        Forma::Platanera,
317        Forma::Roble,
318        Forma::Hongo,
319        Forma::Tocon,
320        Forma::Trigo,
321        Forma::Junco,
322        Forma::Lavanda,
323    ];
324
325    /// Nombre legible (español).
326    pub fn label(self) -> &'static str {
327        match self {
328            Forma::Columnar => "columnar",
329            Forma::Pasto => "grama",
330            Forma::Arbusto => "arbusto",
331            Forma::Arbol => "árbol",
332            Forma::Conifera => "conífera",
333            Forma::Palmera => "palmera",
334            Forma::Helecho => "helecho",
335            Forma::Flor => "flor",
336            Forma::Bambu => "bambú",
337            Forma::Platanera => "platanera",
338            Forma::Roble => "roble",
339            Forma::Hongo => "hongo",
340            Forma::Tocon => "tocón",
341            Forma::Trigo => "trigo",
342            Forma::Junco => "junco",
343            Forma::Lavanda => "lavanda",
344        }
345    }
346
347    /// La forma siguiente (cicla).
348    pub fn next(self) -> Forma {
349        let i = Forma::ALL.iter().position(|&f| f == self).unwrap_or(0);
350        Forma::ALL[(i + 1) % Forma::ALL.len()]
351    }
352}
353
354/// **Material resuelto** para el render: color + grano concretos (ya aplicada la
355/// herencia de [`MaterialDef`](crate::MaterialDef)). Es lo que entra a la paleta.
356#[derive(Debug, Clone, Copy, PartialEq)]
357pub struct ResolvedMaterial {
358    pub color: [u8; 3],
359    pub grain: f32,
360    /// Brillo/reflectividad `[0,1]` (vidrio, acero, cerámica esmaltada, mármol…);
361    /// `0` = mate. Lo lee el ray-march igual que el de los minerales (via el alpha
362    /// del voxel), así que un material lustroso glasea al sol.
363    pub brillo: f32,
364    /// **Detalle sub-voxel** `(rugosidad, bisel)` del material: se hornea POR VOXEL
365    /// en la superficie (via `VoxelGrid::set_detail`), así dos materiales del mismo
366    /// bioma lucen distinto a la vez (rugoso vs liso) — no un look global. `(0,0)` =
367    /// sin detalle (usa el look global del ray-march). Ver `Project::detalle_material`.
368    pub detalle: (f32, f32),
369}
370
371impl ResolvedMaterial {
372    /// Material mate (brillo 0).
373    pub fn new(color: [u8; 3], grain: f32) -> Self {
374        Self { color, grain, brillo: 0.0, detalle: (0.0, 0.0) }
375    }
376
377    /// Material con brillo explícito.
378    pub fn new_b(color: [u8; 3], grain: f32, brillo: f32) -> Self {
379        Self { color, grain, brillo, detalle: (0.0, 0.0) }
380    }
381
382    /// Adjunta el detalle sub-voxel `(rugosidad, bisel)` (builder).
383    pub fn con_detalle(mut self, detalle: (f32, f32)) -> Self {
384        self.detalle = detalle;
385        self
386    }
387}
388
389/// **Uso de un material como objeto** en un bioma: qué material, con qué densidad
390/// `[0,1]` brota, y con qué [`Forma`].
391#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
392pub struct ObjetoUso {
393    pub material: u64,
394    pub densidad: f32,
395    pub forma: Forma,
396}
397
398/// **Uso de un ser** en un bioma: qué ser y con qué probabilidad `[0,1]` puebla.
399/// (Por ahora sólo dato del modelo; el spawn real vendrá con el motor.)
400#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
401pub struct SereUso {
402    pub sere: u64,
403    pub probabilidad: f32,
404}
405
406/// **Paleta resuelta de un bioma**: los colores concretos con los que pintar. La
407/// app la arma resolviendo los ids del bioma contra los materiales del `Project`,
408/// y se la pasa a [`Bioma::generate_window`]. Así worldgen no conoce ids ni herencia.
409#[derive(Debug, Clone)]
410pub struct BiomaPalette {
411    pub ground: ResolvedMaterial,
412    pub cliff: ResolvedMaterial,
413    pub peak: Option<ResolvedMaterial>,
414    /// Color del agua (superficie de ríos/lagos).
415    pub agua: [u8; 3],
416    /// Objetos a esparcir: `(material resuelto, densidad, forma)`.
417    pub objetos: Vec<(ResolvedMaterial, f32, Forma)>,
418}
419
420impl BiomaPalette {
421    /// Paleta de prueba/arranque rápida con un solo material de terreno (sin agua
422    /// teñida especial ni objetos). Útil en tests y fallbacks.
423    pub fn flat(color: [u8; 3], grain: f32) -> Self {
424        let m = ResolvedMaterial::new(color, grain);
425        Self {
426            ground: m,
427            cliff: ResolvedMaterial::new(Material::Rock.color(), Material::Rock.grain()),
428            peak: None,
429            agua: Material::Water.color(),
430            objetos: Vec::new(),
431        }
432    }
433
434    /// Material resuelto de la superficie de la columna en `(wx, wz, y)`: decide
435    /// arena/roca/nieve por altura+pendiente (misma lógica que antes, pero sobre la
436    /// paleta resuelta). `peak_at` viene del bioma.
437    #[allow(clippy::too_many_arguments)]
438    fn surface(
439        &self,
440        peak_at: f32,
441        wx: i32,
442        wz: i32,
443        y: u32,
444        h: u32,
445        slope: f32,
446        dim: [u32; 3],
447        seed: u32,
448    ) -> ResolvedMaterial {
449        let dy = dim[1] as f32;
450        let fh = y as f32 / dy;
451        // Acantilado: la cara superior en pendiente fuerte es roca.
452        if y == h && slope > 2.5 {
453            return self.cliff;
454        }
455        // Cumbre (si la hay).
456        if let Some(pk) = self.peak {
457            if fh > peak_at {
458                return pk;
459            }
460        }
461        // Jitter para que la transición a roca no sea una línea perfecta.
462        let jitter = hash2(wx, wz.wrapping_mul(31).wrapping_add(y as i32), seed ^ 0xABCD) * 0.06 - 0.03;
463        if fh + jitter > 0.72 {
464            return self.cliff;
465        }
466        self.ground
467    }
468}
469
470/// Aplica el **grano de materia** al color base de un vóxel: perturba el brillo
471/// (±`grain`·18%) y mete una pizca de variación por canal, deterministamente por la
472/// posición de mundo. `grain = 0` → color intacto. Función pura → seamless entre
473/// ventanas de streaming.
474fn grained(base: [u8; 3], grain: f32, wx: i32, y: u32, wz: i32, seed: u32) -> [u8; 3] {
475    if grain <= 0.0 {
476        return base;
477    }
478    let g = grain.clamp(0.0, 1.0);
479    let h = hash2(wx, wz.wrapping_mul(31).wrapping_add(y as i32 * 7), seed ^ 0x6817);
480    let bright = 1.0 + (h - 0.5) * 2.0 * g * 0.18; // ±18% a grano pleno
481    let speck = hash2(wx.wrapping_mul(13).wrapping_add(y as i32), wz, seed ^ 0x51A3);
482    let tilt = (speck - 0.5) * 2.0 * g * 12.0; // ±12 niveles, rompe el liso plano
483    let ch = |c: u8, extra: f32| (c as f32 * bright + extra).clamp(0.0, 255.0) as u8;
484    [ch(base[0], tilt), ch(base[1], tilt * 0.7), ch(base[2], -tilt * 0.5)]
485}
486
487/// La **receta de relieve de un bioma**: parámetros del terreno + referencias a los
488/// materiales (por id) que lo pintan. La **semilla NO vive aquí** — la aporta el
489/// [`Mundo`](crate::Mundo) al generar (un mismo bioma con semillas distintas da
490/// mundos distintos). Producí el `VoxelGrid` con [`generate_window`](Self::generate_window),
491/// pasándole la [`BiomaPalette`] resuelta.
492#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct Bioma {
494    /// Id estable (asignado por el [`Project`](crate::Project)).
495    pub id: u64,
496    /// Nombre editable.
497    pub name: String,
498    /// Nivel base del suelo, fracción del alto del mundo `[0,1]` (la llanura).
499    pub base: f32,
500    /// Amplitud de las ondulaciones suaves del suelo (dunas), fracción del alto.
501    pub dune: f32,
502    /// Amplitud de las montañas, fracción del alto.
503    pub relief: f32,
504    /// **Densidad de montañas** `[0,1]`: 0 = casi todo llano, 1 = relieve por todos lados.
505    pub mountains: f32,
506    /// Nivel del agua, fracción del alto `[0,1]`.
507    pub water_level: f32,
508    /// **Densidad de ríos** `[0,1]`.
509    pub rivers: f32,
510    /// Altura normalizada `[0,1]` a partir de la cual aparece la cumbre (`peak`).
511    pub peak_at: f32,
512    /// Material del suelo (id en [`Project::materiales`](crate::Project)).
513    pub ground: u64,
514    /// Material de acantilados/altura (id).
515    pub cliff: u64,
516    /// Material de cumbre por encima de `peak_at` (id); `None` = sin cumbre.
517    pub peak: Option<u64>,
518    /// Objetos a esparcir (flora/props), cada uno con su densidad y forma.
519    #[serde(default)]
520    pub objetos: Vec<ObjetoUso>,
521    /// Minerales embebidos en la roca (vetas, pepitas, estratos…).
522    #[serde(default)]
523    pub minerales: Vec<MineralUso>,
524    /// Casas esparcidas (pueblo/ciudad procedural por semilla).
525    #[serde(default)]
526    pub casas: Vec<crate::casa::CasaUso>,
527    /// **Densidad urbana** `[0,1]`: cuánto del bioma es pueblo. Bajo = aldeas
528    /// aisladas conectadas por rutas; alto = ciudad extendida. Mueve el umbral del
529    /// ruido de manchas de pueblo.
530    #[serde(default = "default_densidad_urbana")]
531    pub densidad_urbana: f32,
532    /// **Fauna** esparcida por semilla: animales voxel (vaca, ciervo, zorro…) que
533    /// pueblan el terreno, como la flora y las casas. Parados sobre el suelo,
534    /// orientados al azar, determinista por posición.
535    #[serde(default)]
536    pub fauna: Vec<crate::bicho::BichoUso>,
537    /// Seres guionados (actores del `Project` por id) para escenas — dato del modelo.
538    #[serde(default)]
539    pub seres: Vec<SereUso>,
540}
541
542fn default_densidad_urbana() -> f32 {
543    0.4
544}
545
546impl Bioma {
547    /// El **planificador urbano** de este bioma para la `seed` y el alto de `dim`:
548    /// retículo de pueblos con la traza limpia y nivelada (plaza, avenidas, cuadras).
549    fn urbe(&self, seed: u32, dim: [u32; 3]) -> crate::urbe::Urbe {
550        crate::urbe::Urbe::new(seed, self.densidad_urbana, dim[1])
551    }
552
553    /// `true` si la columna de mundo `(wx, wz)` cae dentro del **casco urbano** de
554    /// algún pueblo. `densidad_urbana` mueve el radio del casco (aldea ↔ ciudad).
555    /// Delegado al planificador [`crate::urbe`] para que la respuesta coincida
556    /// exactamente con lo que se traza en el terreno.
557    pub fn in_town(&self, seed: u32, wx: i32, wz: i32, dim: [u32; 3]) -> bool {
558        let urbe = self.urbe(seed, dim);
559        let h = |x: i32, z: i32| self.column_height(seed, x, z, dim);
560        urbe.pueblo_en(wx, wz, &h).is_some()
561    }
562
563    /// **Altura del terreno** (índice `y` del voxel sólido superior) en la columna
564    /// de **mundo** `(wx, wz)` para la semilla `seed`. Función pura: mismo punto →
565    /// misma altura en cualquier ventana. Llanura base + dunas + montañas *gated* +
566    /// ríos tallados hacia el agua.
567    pub fn column_height(&self, seed: u32, wx: i32, wz: i32, dim: [u32; 3]) -> u32 {
568        let dy = dim[1] as f32;
569        let scale = world_scale(dim);
570        let (fx, fz) = (wx as f32 * scale, wz as f32 * scale);
571
572        let base = self.base * dy;
573        let dunes = (fbm(fx * 1.7, fz * 1.7, 4, seed ^ 0x11) - 0.5) * 2.0;
574        let dune_h = dunes * self.dune * dy;
575        let c = fbm(fx, fz, 6, seed);
576        let thr = 1.0 - self.mountains.clamp(0.0, 1.0);
577        let m = ((c - thr).max(0.0) / (1.0 - thr).max(1e-3)).clamp(0.0, 1.0);
578        let mtn_h = smooth(m) * self.relief * dy;
579
580        let mut h = base + dune_h + mtn_h;
581
582        if self.rivers > 0.0 {
583            let r = 1.0 - (fbm(fx * 0.8, fz * 0.8, 4, seed ^ 0x77) - 0.5).abs() * 2.0;
584            let width = 0.03 + 0.10 * self.rivers.clamp(0.0, 1.0);
585            if r > 1.0 - width {
586                let t = ((r - (1.0 - width)) / width).clamp(0.0, 1.0);
587                let bed = (self.water_level * dy - 2.0).max(1.0);
588                h += (bed - h) * smooth(t);
589            }
590        }
591
592        (h.clamp(1.0, dy - 1.0)) as u32
593    }
594
595    /// Nivel del agua como índice `y`.
596    #[inline]
597    pub fn water_y(&self, dim: [u32; 3]) -> u32 {
598        (self.water_level * dim[1] as f32) as u32
599    }
600
601    /// `true` si en la columna `(wx, wz)` brota un objeto de densidad `densidad`
602    /// (gate por hash de mundo, sembrado por `salt` para que objetos distintos no se
603    /// pisen). Sólo en suelo seco y llano.
604    fn has_objeto(&self, wx: i32, wz: i32, h: u32, slope: f32, densidad: f32, salt: u32, dim: [u32; 3]) -> bool {
605        if densidad <= 0.0 {
606            return false;
607        }
608        if h <= self.water_y(dim) + 1 || slope > 1.5 {
609            return false;
610        }
611        hash2(wx.wrapping_mul(7), wz.wrapping_mul(13), self.seed_salt(salt)) < densidad
612    }
613
614    /// Mezcla un `salt` con una constante estable para sembrar el gate de objetos.
615    #[inline]
616    fn seed_salt(&self, salt: u32) -> u32 {
617        (0xC4C7u32).wrapping_add(salt.wrapping_mul(0x9E37_79B9))
618    }
619
620    /// **Color de mineral** para el vóxel sólido de mundo `(wx, y, wz)` bajo una
621    /// columna de altura `h`, o `None` si ahí no aflora ningún depósito. La
622    /// profundidad se mide como fracción bajo la superficie; cada mineral sólo
623    /// aparece dentro de su banda `(prof_min, prof_max)`. El primer depósito que
624    /// acierta gana el vóxel.
625    fn mineral_at(&self, wx: i32, y: u32, wz: i32, h: u32, dim: [u32; 3], seed: u32) -> Option<([u8; 3], u8)> {
626        if self.minerales.is_empty() {
627            return None;
628        }
629        let depth = h.saturating_sub(y) as f32 / dim[1] as f32; // 0 en superficie, crece hacia abajo
630        for (i, mu) in self.minerales.iter().enumerate() {
631            if mu.abundancia <= 0.0 || depth < mu.prof_min || depth > mu.prof_max {
632                continue;
633            }
634            let s = seed ^ (0x0E1E_0000u32).wrapping_add((i as u32).wrapping_mul(0x9E37_79B9));
635            if aspecto_hit(mu.aspecto, wx, y, wz, s, mu.abundancia) {
636                // Factores autorables (color/dureza/brillo), no los del enum.
637                let color = mineral_shade(mu.color, mu.dureza, wx, y, wz, s);
638                // El brillo del mineral viaja en el alpha del voxel (lo lee el shader).
639                let alpha = VoxelGrid::shine_to_alpha(mu.brillo);
640                return Some((color, alpha));
641            }
642        }
643        None
644    }
645
646    /// **Construye una ventana del bioma** con la esquina en la columna de mundo
647    /// `origin = [wx, wz]`, semilla `seed` y la [`BiomaPalette`] ya resuelta. Terreno
648    /// + agua + objetos, todo por material. Deja el grid limpio de *dirty*.
649    pub fn generate_window(
650        &self,
651        seed: u32,
652        palette: &BiomaPalette,
653        dim: [u32; 3],
654        origin: [i32; 2],
655    ) -> VoxelGrid {
656        let [dx, dy, dz] = dim;
657        let mut g = VoxelGrid::new(dim);
658        let (ox, oz) = (origin[0], origin[1]);
659        let water_y = self.water_y(dim);
660
661        let h_at = |lx: i32, lz: i32| self.column_height(seed, ox + lx, oz + lz, dim);
662        // Planificador urbano: traza limpia y NIVELADA (plaza, avenidas, cuadras).
663        let urbe = self.urbe(seed, dim);
664        let hw = |wx: i32, wz: i32| self.column_height(seed, wx, wz, dim);
665
666        for lz in 0..dz as i32 {
667            for lx in 0..dx as i32 {
668                let (wx, wz) = (ox + lx, oz + lz);
669                let natural = h_at(lx, lz);
670                let slope = (natural as i32 - h_at(lx - 1, lz) as i32)
671                    .abs()
672                    .max((natural as i32 - h_at(lx, lz - 1) as i32).abs()) as f32;
673
674                // Consulta urbana: dentro de un casco el suelo se aplana a la cota
675                // del pueblo (o rampa en el borde) → el auto rueda sin escalones.
676                let plano = if !self.casas.is_empty() {
677                    urbe.consultar(wx, wz, &hw)
678                } else {
679                    None
680                };
681                // Cota a construir y cómo pintar la superficie: en el casco es una
682                // explanada nivelada (pendiente 0), en el campo el terreno natural.
683                let (build_h, surf_h, surf_slope, urbano) = match &plano {
684                    Some(p) => (p.suelo.min(dy - 1), p.suelo.min(dy - 1), 0.0, true),
685                    None => (natural.min(dy - 1), natural, slope, false),
686                };
687
688                // Columna sólida (con grano por material). Bajo la piel del terreno,
689                // un depósito de mineral puede reemplazar el color de la roca. En el
690                // casco urbano no afloran minerales (es suelo compactado/explanado).
691                for y in 0..=build_h {
692                    let m = palette.surface(self.peak_at, wx, wz, y, surf_h, surf_slope, dim, seed);
693                    let mineral = if !urbano && y + 1 < surf_h {
694                        self.mineral_at(wx, y, wz, surf_h, dim, seed)
695                    } else {
696                        None
697                    };
698                    match mineral {
699                        // Mineral: color + brillo (alpha) del depósito. Hereda el
700                        // detalle sub-voxel de la ROCA en que está embebido (`cliff`)
701                        // → la veta no salta como un parche de otra fidelidad (misma
702                        // gramática cuantizada = sin valle).
703                        Some((color, alpha)) => {
704                            g.set_rgba(lx as u32, y, lz as u32, color, alpha);
705                            let d = palette.cliff.detalle;
706                            if d != (0.0, 0.0) {
707                                g.set_detail(lx as u32, y, lz as u32, d.0, d.1);
708                            }
709                        }
710                        // Material del terreno: mate, salvo los lustrosos (vidrio,
711                        // acero, mármol…) que llevan su brillo en el alpha.
712                        None => {
713                            let c = grained(m.color, m.grain, wx, y, wz, seed);
714                            if m.brillo > 0.0 {
715                                g.set_rgba(lx as u32, y, lz as u32, c, VoxelGrid::shine_to_alpha(m.brillo));
716                            } else {
717                                g.set(lx as u32, y, lz as u32, c);
718                            }
719                            // Detalle sub-voxel POR VOXEL del material (rugoso/liso, biselado
720                            // o duro): así dos materiales del bioma lucen distinto a la vez.
721                            if m.detalle != (0.0, 0.0) {
722                                g.set_detail(lx as u32, y, lz as u32, m.detalle.0, m.detalle.1);
723                            }
724                        }
725                    }
726                }
727                // Agua (sólo en campo abierto; el casco urbano vive sobre explanada seca).
728                if !urbano && build_h < water_y {
729                    for y in (build_h + 1)..=water_y.min(dy - 1) {
730                        g.set(lx as u32, y, lz as u32, palette.agua);
731                    }
732                }
733                // Pavimento de la traza urbana sobre la explanada nivelada.
734                if let Some(pav) = plano.as_ref().and_then(|p| p.pav) {
735                    g.set(lx as u32, build_h, lz as u32, pav);
736                }
737                // Objetos (flora/props): sólo en campo abierto — el casco urbano
738                // queda despejado salvo el arbolado cívico de la plaza.
739                if !urbano {
740                    for (i, (mat, densidad, forma)) in palette.objetos.iter().enumerate() {
741                        if self.has_objeto(wx, wz, natural, slope, *densidad, i as u32, dim) {
742                            // Detalle sub-voxel POR VOXEL de la flora/prop: su material
743                            // lo lleva igual que el terreno (rugoso/liso, biselado o
744                            // duro) → un cactus rugoso junto a roca lisa, a la vez. El
745                            // pincel tiñe todos los voxels que `place_objeto` estampe.
746                            if mat.detalle != (0.0, 0.0) {
747                                g.set_detail_brush(mat.detalle.0, mat.detalle.1);
748                            }
749                            self.place_objeto(&mut g, *forma, mat.color, lx as u32, natural + 1, lz as u32, wx, wz, dim);
750                            g.clear_detail_brush();
751                            break; // un objeto por columna
752                        }
753                    }
754                }
755            }
756        }
757
758        // Casas y mobiliario cívico: una vez por pueblo, todo asentado sobre la
759        // explanada nivelada del casco (misma cota `suelo` que la traza) → nada de
760        // casas escalonadas. Iglesia al norte de la plaza, fuente/bancos/árboles en
761        // la plaza, y una casa por CUADRA (interior de manzana), del estilo del bioma.
762        if !self.casas.is_empty() {
763            let interior = urbe.radio_interior();
764            for p in crate::urbe::pueblos_en_ventana(&urbe, [ox, oz], dim, &hw) {
765                let [cx, cz] = p.centro;
766                let base = p.suelo; // cota nivelada del casco
767                let en_ventana = |wx: i32, wz: i32| -> Option<(u32, u32)> {
768                    let (lx, lz) = (wx - ox, wz - oz);
769                    if lx >= 0 && lz >= 0 && lx < dx as i32 && lz < dz as i32 {
770                        Some((lx as u32, lz as u32))
771                    } else {
772                        None
773                    }
774                };
775
776                // Iglesia: sobre su plot, al norte de la plaza, mirando al sur.
777                if let Some((lx, lz)) = en_ventana(cx, cz + crate::urbe::IGLESIA_V) {
778                    crate::stamp_mueble(&mut g, crate::Mueble::Iglesia, lx, base + 1, lz, 0);
779                }
780                // Fuente al centro de la plaza + bancos + arbolado en las esquinas.
781                if let Some((lx, lz)) = en_ventana(cx, cz) {
782                    crate::stamp_mueble(&mut g, crate::Mueble::Fuente, lx, base + 1, lz, 0);
783                }
784                for (bi, &(bx, bz)) in [(-4i32, 0i32), (4, 0), (0, 4)].iter().enumerate() {
785                    if let Some((lx, lz)) = en_ventana(cx + bx, cz + bz) {
786                        crate::stamp_mueble(&mut g, crate::Mueble::Banco, lx, base + 1, lz, bi as u8);
787                    }
788                }
789                let ph = crate::urbe::PLAZA_HALF;
790                for &(tx, tz) in &[(-ph, -ph), (ph, -ph), (-ph, ph), (ph, ph)] {
791                    if let Some((lx, lz)) = en_ventana(cx + tx, cz + tz) {
792                        crate::stamp_mueble(&mut g, crate::Mueble::Ficus, lx, base + 1, lz, 0);
793                    }
794                }
795
796                // Una casa por cuadra: centro de manzana (offset medio paso desde las
797                // líneas de la grilla) dentro del radio interior, salteando plaza,
798                // iglesia y calzada.
799                let paso = crate::urbe::PASO_CUADRA;
800                let n = interior / paso + 1;
801                for ku in -n..=n {
802                    for kv in -n..=n {
803                        let bu = ku * paso + paso / 2; // centro de cuadra (local)
804                        let bv = kv * paso + paso / 2;
805                        if bu * bu + bv * bv > interior * interior {
806                            continue; // fuera del casco urbanizable
807                        }
808                        if urbe.zona(bu, bv) != crate::urbe::Zona::Parcela {
809                            continue; // cayó en plaza/calzada
810                        }
811                        let (cwx, cwz) = (cx + bu, cz + bv);
812                        let (lx, lz) = match en_ventana(cwx, cwz) {
813                            Some(v) => v,
814                            None => continue,
815                        };
816                        for (i, cu) in self.casas.iter().enumerate() {
817                            if cu.densidad <= 0.0 {
818                                continue;
819                            }
820                            let gate = hash2(cwx.wrapping_mul(13), cwz.wrapping_mul(7), p.seed ^ (0x50 + i as u32));
821                            if gate < cu.densidad {
822                                let cseed = crate::casa::casa_seed(seed, cwx, cwz);
823                                crate::casa::stamp_casa(&mut g, cu.tipo, lx, base + 1, lz, cseed);
824                                break; // una casa por cuadra
825                            }
826                        }
827                    }
828                }
829            }
830        }
831
832        // Fauna: animales esparcidos por semilla en una grilla propia (más fina que
833        // la de casas). Cada parcela con acierto pone un ser parado sobre el suelo,
834        // orientado por la posición. Los seres viven en el campo, no en la calzada.
835        if !self.fauna.is_empty() {
836            let plot = 11i32;
837            let ix0 = ox.div_euclid(plot);
838            let ix1 = (ox + dx as i32).div_euclid(plot);
839            let iz0 = oz.div_euclid(plot);
840            let iz1 = (oz + dz as i32).div_euclid(plot);
841            for ix in ix0..=ix1 {
842                for iz in iz0..=iz1 {
843                    // Jitter del punto dentro de la parcela → rebaño no cuadriculado.
844                    let jx = (hash2(ix.wrapping_mul(5), iz.wrapping_mul(9), self.seed_salt(0x71) ^ seed) * plot as f32) as i32;
845                    let jz = (hash2(ix.wrapping_mul(11), iz.wrapping_mul(3), self.seed_salt(0x72) ^ seed) * plot as f32) as i32;
846                    let cwx = ix * plot + jx;
847                    let cwz = iz * plot + jz;
848                    let (lx, lz) = (cwx - ox, cwz - oz);
849                    if lx < 0 || lz < 0 || lx >= dx as i32 || lz >= dz as i32 {
850                        continue;
851                    }
852                    let hh = self.column_height(seed, cwx, cwz, dim);
853                    if hh <= water_y + 1 || hh >= dy {
854                        continue; // agua/muy bajo
855                    }
856                    // Sin fauna sobre la calzada (rutas/calles) ni en pendiente fuerte.
857                    let slope = (hh as i32 - self.column_height(seed, cwx - 1, cwz, dim) as i32)
858                        .abs()
859                        .max((hh as i32 - self.column_height(seed, cwx, cwz - 1, dim) as i32).abs());
860                    if slope > 2 {
861                        continue;
862                    }
863                    let ruta = !self.casas.is_empty()
864                        && (cwx.rem_euclid(48) < 5 || cwz.rem_euclid(48) < 5);
865                    if ruta {
866                        continue;
867                    }
868                    let facing = (hash2(cwx, cwz, self.seed_salt(0x7F) ^ seed) * 4.0) as u8 & 3;
869                    for (i, fu) in self.fauna.iter().enumerate() {
870                        if fu.densidad <= 0.0 {
871                            continue;
872                        }
873                        let gate = hash2(cwx.wrapping_mul(17), cwz.wrapping_mul(11), self.seed_salt(0x70 + i as u32) ^ seed);
874                        if gate < fu.densidad {
875                            // Un ser vivo lleva su gramática sub-voxel (redondeo orgánico
876                            // + grano de piel) → deja de ser una caja rectangular. El
877                            // pincel tiñe todo el cuerpo que `stamp_bicho` estampe.
878                            let (r, b) = crate::bicho::DETALLE_SER_VIVO;
879                            g.set_detail_brush(r, b);
880                            crate::bicho::stamp_bicho(&mut g, fu.bicho, lx as u32, hh + 1, lz as u32, facing);
881                            g.clear_detail_brush();
882                            break; // un ser por parcela
883                        }
884                    }
885                }
886            }
887        }
888
889        g.reset_dirty();
890        g
891    }
892
893    /// Estampa una **sola** forma de vegetación con follaje `col` y base en
894    /// `(x, base_y, z)` (local), determinista por esa posición. Pensado para
895    /// catálogos/tests; la generación normal usa [`generate_window`](Self::generate_window).
896    pub fn stamp_flora(
897        &self,
898        g: &mut VoxelGrid,
899        forma: Forma,
900        col: [u8; 3],
901        x: u32,
902        base_y: u32,
903        z: u32,
904        dim: [u32; 3],
905    ) {
906        self.place_objeto(g, forma, col, x, base_y, z, x as i32, z as i32, dim);
907    }
908
909    /// Coloca un objeto de vegetación con la base en `(x, base_y, z)` (local),
910    /// despachando por [`Forma`]. `col` es el **color de follaje** (del material del
911    /// objeto); los troncos leñosos usan [`WOOD`]. Todo es determinista por la
912    /// posición de mundo `(wx, wz)` → seamless entre ventanas de streaming. Los
913    /// vóxels fuera de la ventana se recortan (igual que el cactus histórico).
914    #[allow(clippy::too_many_arguments)]
915    fn place_objeto(
916        &self,
917        g: &mut VoxelGrid,
918        forma: Forma,
919        col: [u8; 3],
920        x: u32,
921        base_y: u32,
922        z: u32,
923        wx: i32,
924        wz: i32,
925        dim: [u32; 3],
926    ) {
927        let cx = x as i32;
928        let cz = z as i32;
929        let by = base_y as i32;
930        // Follaje con jitter de brillo por vóxel → copas menos «plásticas».
931        let leaf = |xi: i32, yi: i32, zi: i32| leaf_tint(col, wx + (xi - cx), yi as u32, wz + (zi - cz), 0x1EAF);
932
933        match forma {
934            Forma::Columnar => {
935                let th = 3 + (hash2(wx, wz, 0x1357) * 4.0) as i32;
936                for k in 0..th {
937                    put(g, dim, cx, by + k, cz, col);
938                }
939                let arm_seed = hash2(wx.wrapping_add(1), wz, 0x2468);
940                if th >= 4 && arm_seed > 0.45 {
941                    let ay = by + th / 2;
942                    put(g, dim, cx + 1, ay, cz, col);
943                    put(g, dim, cx + 1, ay + 1, cz, col);
944                    if arm_seed > 0.7 {
945                        put(g, dim, cx - 1, ay + 1, cz, col);
946                        put(g, dim, cx - 1, ay + 2, cz, col);
947                    }
948                }
949            }
950
951            Forma::Pasto => {
952                let n = 3 + (hash2(wx, wz, 0x9E13) * 4.0) as i32; // 3..6 matas
953                for i in 0..n {
954                    let ox = (hash2(wx, wz.wrapping_add(i), 0x31) * 3.0) as i32 - 1; // -1..1
955                    let oz = (hash2(wx.wrapping_add(7), wz.wrapping_add(i), 0x59) * 3.0) as i32 - 1;
956                    let bh = 1 + (hash2(wx.wrapping_add(i), wz, 0x77) * 2.0) as i32; // 1..2
957                    for k in 0..bh {
958                        put(g, dim, cx + ox, by + k, cz + oz, leaf(cx + ox, by + k, cz + oz));
959                    }
960                }
961            }
962
963            Forma::Arbusto => {
964                let r = 2i32;
965                let cy = by + 2;
966                for yy in by..=by + 3 {
967                    for xx in -r..=r {
968                        for zz in -r..=r {
969                            let d2 = xx * xx + (yy - cy) * (yy - cy) + zz * zz;
970                            if d2 <= r * r + 1
971                                && hash2(wx + xx, wz.wrapping_add(zz).wrapping_add(yy), 0x2B) > 0.18
972                            {
973                                put(g, dim, cx + xx, yy, cz + zz, leaf(cx + xx, yy, cz + zz));
974                            }
975                        }
976                    }
977                }
978            }
979
980            Forma::Arbol => {
981                let th = 4 + (hash2(wx, wz, 0x1357) * 5.0) as i32; // 4..8 tronco
982                for k in 0..th {
983                    put(g, dim, cx, by + k, cz, WOOD);
984                }
985                let cy = by + th; // centro de copa en la cima del tronco
986                let r = 2 + (hash2(wx.wrapping_add(3), wz, 0x246) * 2.0) as i32; // 2..3
987                for xx in -r..=r {
988                    for yy in -r..=r {
989                        for zz in -r..=r {
990                            let d2 = xx * xx + yy * yy + zz * zz;
991                            if d2 <= r * r + r
992                                && hash2(wx + xx, wz.wrapping_add(zz).wrapping_add(yy), 0x9C) > 0.12
993                            {
994                                put(g, dim, cx + xx, cy + yy, cz + zz, leaf(cx + xx, cy + yy, cz + zz));
995                            }
996                        }
997                    }
998                }
999            }
1000
1001            Forma::Conifera => {
1002                let h = 6 + (hash2(wx, wz, 0x1357) * 5.0) as i32; // 6..10 de follaje
1003                put(g, dim, cx, by, cz, WOOD); // tronquito visible
1004                let maxr = 3i32;
1005                for layer in 0..h {
1006                    let yy = by + 1 + layer;
1007                    let r = maxr * (h - layer) / h; // decrece hacia arriba
1008                    for xx in -r..=r {
1009                        for zz in -r..=r {
1010                            if xx * xx + zz * zz <= r * r + 1
1011                                && hash2(wx + xx, wz.wrapping_add(zz).wrapping_add(yy), 0x5C1) > 0.10
1012                            {
1013                                put(g, dim, cx + xx, yy, cz + zz, leaf(cx + xx, yy, cz + zz));
1014                            }
1015                        }
1016                    }
1017                }
1018                put(g, dim, cx, by + 1 + h, cz, leaf(cx, by + 1 + h, cz)); // punta
1019            }
1020
1021            Forma::Palmera => {
1022                let h = 7 + (hash2(wx, wz, 0x1357) * 6.0) as i32; // 7..12 tronco
1023                let lean = if hash2(wx, wz, 0x9) > 0.5 { 1 } else { -1 };
1024                let mut tx = cx;
1025                for k in 0..h {
1026                    if k == h * 2 / 3 {
1027                        tx += lean; // leve curva cerca de la corona
1028                    }
1029                    put(g, dim, tx, by + k, cz, WOOD);
1030                }
1031                let ty = by + h;
1032                put(g, dim, tx, ty, cz, leaf(tx, ty, cz)); // corona
1033                // Frondas en 8 rumbos: suben 1 y luego caen (arco).
1034                let dirs = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)];
1035                for (dxi, dzi) in dirs {
1036                    let len = 3 + (hash2(tx + dxi, cz + dzi, 0x33) * 2.0) as i32; // 3..4
1037                    for s in 1..=len {
1038                        let fx = tx + dxi * s;
1039                        let fz = cz + dzi * s;
1040                        let fy = ty + 1 - s; // arco: s=1→ty, luego baja
1041                        put(g, dim, fx, fy, fz, leaf(fx, fy, fz));
1042                    }
1043                }
1044            }
1045
1046            Forma::Helecho => {
1047                let sh = 2i32;
1048                for k in 0..sh {
1049                    put(g, dim, cx, by + k, cz, leaf(cx, by + k, cz));
1050                }
1051                let ty = by + sh;
1052                let dirs = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, -1)];
1053                for (dxi, dzi) in dirs {
1054                    let len = 2 + (hash2(wx + dxi, wz + dzi, 0x71) * 2.0) as i32; // 2..3
1055                    for s in 1..=len {
1056                        let fy = (ty - s / 2).max(by); // frondas que arquean hacia abajo
1057                        put(g, dim, cx + dxi * s, fy, cz + dzi * s, leaf(cx + dxi * s, fy, cz + dzi * s));
1058                    }
1059                }
1060            }
1061
1062            Forma::Flor => {
1063                let sh = 2 + (hash2(wx, wz, 0x5) * 2.0) as i32; // 2..3 tallo
1064                for k in 0..sh {
1065                    put(g, dim, cx, by + k, cz, STEM);
1066                }
1067                let ty = by + sh;
1068                put(g, dim, cx, ty, cz, col); // corola (color del material)
1069                for (dxi, dzi) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
1070                    put(g, dim, cx + dxi, ty, cz + dzi, col); // pétalos en cruz
1071                }
1072            }
1073
1074            Forma::Bambu => {
1075                let n = 3 + (hash2(wx, wz, 0xB00) * 3.0) as i32; // 3..5 cañas
1076                for i in 0..n {
1077                    let ox = (hash2(wx.wrapping_add(i), wz, 0xB01) * 3.0) as i32 - 1;
1078                    let oz = (hash2(wx, wz.wrapping_add(i), 0xB02) * 3.0) as i32 - 1;
1079                    let ch = 7 + (hash2(wx.wrapping_add(i * 3), wz, 0xB03) * 6.0) as i32; // 7..12
1080                    for k in 0..ch {
1081                        put(g, dim, cx + ox, by + k, cz + oz, leaf(cx + ox, by + k, cz + oz));
1082                    }
1083                    // Hojitas en la punta.
1084                    put(g, dim, cx + ox + 1, by + ch - 1, cz + oz, leaf(cx + ox + 1, by + ch - 1, cz + oz));
1085                    put(g, dim, cx + ox, by + ch, cz + oz + 1, leaf(cx + ox, by + ch, cz + oz + 1));
1086                }
1087            }
1088
1089            Forma::Platanera => {
1090                let h = 3 + (hash2(wx, wz, 0x9A0) * 2.0) as i32; // 3..4 pseudo-tronco verde
1091                for k in 0..h {
1092                    put(g, dim, cx, by + k, cz, leaf(cx, by + k, cz));
1093                }
1094                let ty = by + h;
1095                // Hojas anchas (2 de ancho) que suben 1 y caen despacio.
1096                let dirs = [(1i32, 0i32), (-1, 0), (0, 1), (0, -1), (1, 1)];
1097                for (dxi, dzi) in dirs {
1098                    let len = 4 + (hash2(cx + dxi, cz + dzi, 0x9A1) * 2.0) as i32; // 4..5
1099                    let (px, pz) = (-dzi, dxi); // perpendicular → ancho de hoja
1100                    for s in 1..=len {
1101                        let fy = ty + 1 - (s + 1) / 2;
1102                        let (bx, bz) = (cx + dxi * s, cz + dzi * s);
1103                        put(g, dim, bx, fy, bz, leaf(bx, fy, bz));
1104                        if s >= 2 {
1105                            put(g, dim, bx + px, fy, bz + pz, leaf(bx + px, fy, bz + pz));
1106                        }
1107                    }
1108                }
1109            }
1110
1111            Forma::Roble => {
1112                let th = 5 + (hash2(wx, wz, 0x0A0) * 4.0) as i32; // 5..8 tronco grueso
1113                for k in 0..th {
1114                    put(g, dim, cx, by + k, cz, WOOD);
1115                    if k < th * 2 / 3 {
1116                        put(g, dim, cx + 1, by + k, cz, WOOD);
1117                        put(g, dim, cx, by + k, cz + 1, WOOD);
1118                        put(g, dim, cx + 1, by + k, cz + 1, WOOD);
1119                    }
1120                }
1121                let cy = by + th + 1;
1122                let r = 4i32; // copa amplia, achatada
1123                for xx in -r..=r {
1124                    for yy in -3..=3 {
1125                        for zz in -r..=r {
1126                            let d2 = xx * xx + yy * yy * 2 + zz * zz;
1127                            if d2 <= r * r + 2
1128                                && hash2(wx + xx, wz.wrapping_add(zz).wrapping_add(yy), 0x0A1) > 0.14
1129                            {
1130                                put(g, dim, cx + xx, cy + yy, cz + zz, leaf(cx + xx, cy + yy, cz + zz));
1131                            }
1132                        }
1133                    }
1134                }
1135            }
1136
1137            Forma::Hongo => {
1138                let sh = 1 + (hash2(wx, wz, 0xF00) * 2.0) as i32; // 1..2 pie
1139                for k in 0..sh {
1140                    put(g, dim, cx, by + k, cz, MUSH_STEM);
1141                }
1142                let ty = by + sh;
1143                let r = 1 + (hash2(wx, wz, 0xF01) * 1.5) as i32; // sombrero radio 1..2
1144                for xx in -r..=r {
1145                    for zz in -r..=r {
1146                        if xx * xx + zz * zz <= r * r + 1 {
1147                            put(g, dim, cx + xx, ty, cz + zz, FUNGUS_CAP);
1148                        }
1149                    }
1150                }
1151                put(g, dim, cx, ty + 1, cz, FUNGUS_CAP); // domo
1152            }
1153
1154            Forma::Tocon => {
1155                let h = 2i32; // cepa baja y gruesa
1156                for yy in 0..h {
1157                    for xx in -1..=1 {
1158                        for zz in -1..=1 {
1159                            if xx * xx + zz * zz <= 2 {
1160                                put(g, dim, cx + xx, by + yy, cz + zz, WOOD);
1161                            }
1162                        }
1163                    }
1164                }
1165                // Musgo en la cara superior.
1166                for xx in -1..=1 {
1167                    for zz in -1..=1 {
1168                        if xx * xx + zz * zz <= 2
1169                            && hash2(wx + xx, wz.wrapping_add(zz), 0x70C) > 0.4
1170                        {
1171                            put(g, dim, cx + xx, by + h, cz + zz, leaf(cx + xx, by + h, cz + zz));
1172                        }
1173                    }
1174                }
1175            }
1176
1177            Forma::Trigo => {
1178                let n = 4 + (hash2(wx, wz, 0x7A0) * 4.0) as i32; // 4..7 espigas
1179                for i in 0..n {
1180                    let ox = (hash2(wx.wrapping_add(i), wz, 0x7A1) * 3.0) as i32 - 1;
1181                    let oz = (hash2(wx, wz.wrapping_add(i), 0x7A2) * 3.0) as i32 - 1;
1182                    let sh = 3 + (hash2(wx.wrapping_add(i * 2), wz, 0x7A3) * 2.0) as i32; // 3..4
1183                    for k in 0..sh {
1184                        put(g, dim, cx + ox, by + k, cz + oz, leaf(cx + ox, by + k, cz + oz));
1185                    }
1186                    put(g, dim, cx + ox, by + sh, cz + oz, col); // espiga (grano dorado)
1187                }
1188            }
1189
1190            Forma::Junco => {
1191                let n = 4 + (hash2(wx, wz, 0x5A0) * 4.0) as i32; // 4..7 cañas
1192                for i in 0..n {
1193                    let ox = (hash2(wx.wrapping_add(i), wz, 0x5A1) * 3.0) as i32 - 1;
1194                    let oz = (hash2(wx, wz.wrapping_add(i), 0x5A2) * 3.0) as i32 - 1;
1195                    let sh = 4 + (hash2(wx.wrapping_add(i * 2), wz, 0x5A3) * 3.0) as i32; // 4..6
1196                    for k in 0..sh {
1197                        put(g, dim, cx + ox, by + k, cz + oz, leaf(cx + ox, by + k, cz + oz));
1198                    }
1199                    // Espadaña marrón (cattail) en el tope.
1200                    put(g, dim, cx + ox, by + sh, cz + oz, CATTAIL);
1201                    put(g, dim, cx + ox, by + sh - 1, cz + oz, CATTAIL);
1202                }
1203            }
1204
1205            Forma::Lavanda => {
1206                let r = 1i32; // matojo bajo verde
1207                for xx in -r..=r {
1208                    for zz in -r..=r {
1209                        put(g, dim, cx + xx, by, cz + zz, STEM);
1210                        // Espigas florales en tresbolillo.
1211                        if (xx + zz).rem_euclid(2) == 0 {
1212                            put(g, dim, cx + xx, by + 1, cz + zz, col);
1213                            if hash2(wx + xx, wz.wrapping_add(zz), 0x1A7) > 0.5 {
1214                                put(g, dim, cx + xx, by + 2, cz + zz, col);
1215                            }
1216                        }
1217                    }
1218                }
1219            }
1220        }
1221    }
1222}
1223
1224/// Color de tronco/madera para las formas leñosas (árbol, palmera, conífera).
1225const WOOD: [u8; 3] = [102, 72, 44];
1226
1227/// Color de tallo verde para las flores (independiente del color de la corola).
1228const STEM: [u8; 3] = [78, 130, 60];
1229
1230/// Pie pálido del hongo.
1231const MUSH_STEM: [u8; 3] = [216, 206, 182];
1232
1233/// Sombrero del hongo (rojo clásico) — color intrínseco de la forma.
1234const FUNGUS_CAP: [u8; 3] = [176, 58, 48];
1235
1236/// Espadaña marrón del junco (cattail).
1237const CATTAIL: [u8; 3] = [122, 82, 46];
1238
1239/// Escribe un vóxel en coordenadas locales con signo, recortando fuera de la ventana.
1240#[inline]
1241fn put(g: &mut VoxelGrid, dim: [u32; 3], xi: i32, yi: i32, zi: i32, c: [u8; 3]) {
1242    if xi >= 0
1243        && yi >= 0
1244        && zi >= 0
1245        && (xi as u32) < dim[0]
1246        && (yi as u32) < dim[1]
1247        && (zi as u32) < dim[2]
1248    {
1249        g.set(xi as u32, yi as u32, zi as u32, c);
1250    }
1251}
1252
1253/// Perturba un color de follaje ±15% de brillo por vóxel, determinista por la
1254/// posición de mundo → copas con variación de hoja en vez de un verde plano.
1255fn leaf_tint(base: [u8; 3], wx: i32, y: u32, wz: i32, seed: u32) -> [u8; 3] {
1256    let h = hash2(wx.wrapping_mul(9).wrapping_add(y as i32), wz.wrapping_mul(5), seed ^ 0x7A31);
1257    let f = 0.85 + h * 0.30; // 0.85..1.15
1258    let ch = |c: u8| (c as f32 * f).clamp(0.0, 255.0) as u8;
1259    [ch(base[0]), ch(base[1]), ch(base[2])]
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264    use super::*;
1265
1266    /// Un bioma de desierto de prueba (ids ficticios; la generación no los usa, usa
1267    /// la paleta) + su paleta de arena/roca.
1268    fn desierto() -> (Bioma, BiomaPalette) {
1269        let b = Bioma {
1270            id: 1,
1271            name: "desierto".into(),
1272            base: 0.30,
1273            dune: 0.05,
1274            relief: 0.45,
1275            mountains: 0.12,
1276            water_level: 0.26,
1277            rivers: 0.18,
1278            peak_at: 1.0,
1279            ground: 1,
1280            cliff: 2,
1281            peak: None,
1282            objetos: vec![ObjetoUso { material: 3, densidad: 0.010, forma: Forma::Columnar }],
1283            minerales: vec![],
1284            densidad_urbana: 0.4,
1285            casas: Vec::new(),
1286            fauna: vec![],
1287            seres: vec![],
1288        };
1289        let pal = BiomaPalette {
1290            ground: ResolvedMaterial::new(Material::Sand.color(), Material::Sand.grain()),
1291            cliff: ResolvedMaterial::new(Material::Rock.color(), Material::Rock.grain()),
1292            peak: None,
1293            agua: Material::Water.color(),
1294            objetos: vec![(ResolvedMaterial::new(Material::Cactus.color(), 0.0), 0.010, Forma::Columnar)],
1295        };
1296        (b, pal)
1297    }
1298
1299    fn pradera() -> (Bioma, BiomaPalette) {
1300        let b = Bioma {
1301            id: 2,
1302            name: "pradera".into(),
1303            base: 0.22,
1304            dune: 0.10,
1305            relief: 0.7,
1306            mountains: 0.5,
1307            water_level: 0.30,
1308            rivers: 0.25,
1309            peak_at: 0.80,
1310            ground: 4,
1311            cliff: 2,
1312            peak: Some(5),
1313            objetos: vec![],
1314            minerales: vec![],
1315            densidad_urbana: 0.4,
1316            casas: Vec::new(),
1317            fauna: vec![],
1318            seres: vec![],
1319        };
1320        let pal = BiomaPalette {
1321            ground: ResolvedMaterial::new(Material::Grass.color(), Material::Grass.grain()),
1322            cliff: ResolvedMaterial::new(Material::Rock.color(), Material::Rock.grain()),
1323            peak: Some(ResolvedMaterial::new(Material::Snow.color(), Material::Snow.grain())),
1324            agua: Material::Water.color(),
1325            objetos: vec![],
1326        };
1327        (b, pal)
1328    }
1329
1330    #[test]
1331    fn altura_es_funcion_pura_de_mundo() {
1332        let (b, _) = desierto();
1333        let dim = [96, 48, 96];
1334        assert_eq!(b.column_height(7, 1234, -567, dim), b.column_height(7, 1234, -567, dim));
1335    }
1336
1337    #[test]
1338    fn desierto_es_mas_llano_que_pradera() {
1339        let dim = [128, 56, 128];
1340        let var = |b: &Bioma| {
1341            let mut hs = Vec::new();
1342            for z in (0..400).step_by(7) {
1343                for x in (0..400).step_by(7) {
1344                    hs.push(b.column_height(42, x, z, dim) as f32);
1345                }
1346            }
1347            let mean = hs.iter().sum::<f32>() / hs.len() as f32;
1348            hs.iter().map(|h| (h - mean).powi(2)).sum::<f32>() / hs.len() as f32
1349        };
1350        let d = var(&desierto().0);
1351        let g = var(&pradera().0);
1352        assert!(d < g, "el desierto es más llano: var {d:.1} vs {g:.1}");
1353    }
1354
1355    #[test]
1356    fn el_desierto_pinta_arena_agua_y_cactus() {
1357        let dim = [128, 48, 128];
1358        let (b, pal) = desierto();
1359        let grid = b.generate_window(3, &pal, dim, [0, 0]);
1360        let mut seen = [false; 3]; // [arena, agua, cactus]
1361        let near = |a: [u8; 3], b: [u8; 3]| {
1362            (a[0] as i32 - b[0] as i32).abs() <= 60
1363                && (a[1] as i32 - b[1] as i32).abs() <= 60
1364                && (a[2] as i32 - b[2] as i32).abs() <= 60
1365        };
1366        for z in 0..dim[2] {
1367            for x in 0..dim[0] {
1368                for y in 0..dim[1] {
1369                    if let Some(c) = grid.get(x, y, z) {
1370                        if c[3] == 0 {
1371                            continue;
1372                        }
1373                        let rgb = [c[0], c[1], c[2]];
1374                        if rgb == Material::Water.color() {
1375                            seen[1] = true;
1376                        } else if rgb == Material::Cactus.color() {
1377                            seen[2] = true;
1378                        } else if near(rgb, Material::Sand.color()) {
1379                            seen[0] = true;
1380                        }
1381                    }
1382                }
1383            }
1384        }
1385        assert!(seen[0], "hay arena");
1386        assert!(seen[1], "hay agua (ríos)");
1387        assert!(seen[2], "hay cactus");
1388    }
1389
1390    #[test]
1391    fn grano_de_materia_perturba_pero_es_determinista() {
1392        let base = Material::Sand.color();
1393        assert_eq!(grained(base, 0.0, 5, 3, 9, 1337), base);
1394        let g1 = grained(base, 0.6, 5, 3, 9, 1337);
1395        assert_ne!(g1, base);
1396        let g2 = grained(base, 0.6, 5, 3, 9, 1337);
1397        assert_eq!(g1, g2);
1398        let gn = grained(base, 0.6, 6, 3, 9, 1337);
1399        assert_ne!(g1, gn);
1400        assert!(Material::Sand.grain() > 0.0);
1401    }
1402
1403    #[test]
1404    fn mismo_seed_mismo_mundo() {
1405        let dim = [64, 40, 64];
1406        let (b, pal) = desierto();
1407        let a = b.generate_window(99, &pal, dim, [0, 0]);
1408        let c = b.generate_window(99, &pal, dim, [0, 0]);
1409        for (x, y, z) in [(10, 12, 10), (30, 8, 44), (60, 20, 5)] {
1410            assert_eq!(a.get(x, y, z), c.get(x, y, z), "({x},{y},{z}) difiere");
1411        }
1412    }
1413
1414    /// Cuenta vóxels sólidos en un grid.
1415    fn solids(g: &VoxelGrid, dim: [u32; 3]) -> usize {
1416        let mut n = 0;
1417        for x in 0..dim[0] {
1418            for y in 0..dim[1] {
1419                for z in 0..dim[2] {
1420                    if g.is_solid(x as i32, y as i32, z as i32) {
1421                        n += 1;
1422                    }
1423                }
1424            }
1425        }
1426        n
1427    }
1428
1429    #[test]
1430    fn cada_forma_estampa_voxels() {
1431        let (b, _) = pradera();
1432        let dim = [48, 40, 48];
1433        let leaf = Material::Leaf.color();
1434        // Cada variante debe dejar al menos unos cuantos vóxels sobre el suelo.
1435        for forma in Forma::ALL {
1436            let mut g = VoxelGrid::new(dim);
1437            b.stamp_flora(&mut g, forma, leaf, 24, 4, 24, dim);
1438            let n = solids(&g, dim);
1439            assert!(n >= 3, "{} estampó sólo {n} vóxels", forma.label());
1440        }
1441    }
1442
1443    #[test]
1444    fn formas_lenosas_tienen_tronco() {
1445        let (b, _) = pradera();
1446        let dim = [48, 40, 48];
1447        let leaf = Material::Leaf.color();
1448        // Árbol, conífera y palmera arrancan de un tronco leñoso (WOOD) sobre la base.
1449        for forma in [Forma::Arbol, Forma::Conifera, Forma::Palmera] {
1450            let mut g = VoxelGrid::new(dim);
1451            b.stamp_flora(&mut g, forma, leaf, 24, 4, 24, dim);
1452            let base = g.get(24, 4, 24).map(|c| [c[0], c[1], c[2]]);
1453            assert_eq!(base, Some(WOOD), "{} no tiene tronco en la base", forma.label());
1454        }
1455    }
1456
1457    #[test]
1458    fn el_pueblo_traza_calles_y_plazas() {
1459        let dim = [64, 40, 64];
1460        let (mut b, pal) = pradera();
1461        b.relief = 0.2;
1462        b.mountains = 0.1;
1463        b.water_level = 0.0;
1464        b.rivers = 0.0;
1465        b.casas = vec![crate::casa::CasaUso { tipo: crate::casa::CasaTipo::Cabana, densidad: 0.6 }];
1466        b.densidad_urbana = 0.95; // toda la ventana es pueblo → calles locales garantizadas
1467        let g = b.generate_window(5, &pal, dim, [0, 0]);
1468        // Hay asfalto de calle en la superficie.
1469        let mut asfalto = 0;
1470        for x in 0..dim[0] {
1471            for y in 0..dim[1] {
1472                for z in 0..dim[2] {
1473                    if g.get(x, y, z) == Some([64, 66, 72, 255]) {
1474                        asfalto += 1;
1475                    }
1476                }
1477            }
1478        }
1479        assert!(asfalto > 20, "el pueblo trazó calles (asfalto = {asfalto})");
1480    }
1481
1482    #[test]
1483    fn el_casco_urbano_es_plano_sin_escalones() {
1484        // Bioma ondulado (dunas + relieve): en campo el suelo sube y baja, pero la
1485        // avenida del casco debe salir PLANA — cero escalones entre columnas vecinas.
1486        let dim = [176, 48, 176];
1487        let (mut b, pal) = pradera();
1488        b.base = 0.2;
1489        b.dune = 0.06; // ondulación marcada del terreno
1490        b.relief = 0.3;
1491        b.mountains = 0.2;
1492        b.water_level = 0.0;
1493        b.rivers = 0.0;
1494        b.densidad_urbana = 0.9;
1495        b.casas = vec![crate::casa::CasaUso { tipo: crate::casa::CasaTipo::Cabana, densidad: 0.5 }];
1496        let seed = 9;
1497        let g = b.generate_window(seed, &pal, dim, [0, 0]);
1498
1499        // Ubicamos un pueblo cuyo centro caiga cómodo dentro de la ventana.
1500        let urbe = b.urbe(seed, dim);
1501        let h = |x: i32, z: i32| b.column_height(seed, x, z, dim);
1502        let m = urbe.radio_interior();
1503        let p = crate::urbe::pueblos_en_ventana(&urbe, [0, 0], dim, &h)
1504            .into_iter()
1505            .find(|p| {
1506                p.centro[0] - m >= 0
1507                    && p.centro[1] - m >= 0
1508                    && p.centro[0] + m < dim[0] as i32
1509                    && p.centro[1] + m < dim[2] as i32
1510            })
1511            .expect("hay un pueblo entero dentro de la ventana");
1512
1513        // Recorremos la avenida central (v=0) por el interior y exigimos cota
1514        // constante: el voxel sólido más alto de cada columna de calzada = la
1515        // explanada. Cero escalones entre columnas vecinas.
1516        let base = p.suelo;
1517        let mut vistos = 0;
1518        let mut escalones = 0;
1519        let mut previa: Option<u32> = None;
1520        for du in -urbe.radio_interior()..=urbe.radio_interior() {
1521            let lx = p.centro[0] + du;
1522            let lz = p.centro[1];
1523            if lx < 0 || lz < 0 || lx >= dim[0] as i32 || lz >= dim[2] as i32 {
1524                continue;
1525            }
1526            let y = match g.height_at(lx as u32, lz as u32) {
1527                Some(y) => y,
1528                None => continue,
1529            };
1530            // Sólo contamos columnas de pura calzada (a cota base, sin mueble encima).
1531            if y == base {
1532                vistos += 1;
1533                if let Some(pr) = previa {
1534                    if pr != y {
1535                        escalones += 1;
1536                    }
1537                }
1538                previa = Some(y);
1539            }
1540        }
1541        assert!(vistos > 20, "la avenida nivelada apenas se vio ({vistos} columnas a cota)");
1542        assert_eq!(escalones, 0, "la avenida del casco tiene escalones");
1543
1544        // La misma semilla da el mismo casco (streaming determinista).
1545        let g2 = b.generate_window(seed, &pal, dim, [0, 0]);
1546        assert_eq!(g.get(p.centro[0] as u32, base, p.centro[1] as u32), g2.get(p.centro[0] as u32, base, p.centro[1] as u32));
1547    }
1548
1549    #[test]
1550    fn la_fauna_puebla_el_bioma() {
1551        let dim = [64, 40, 64];
1552        let (mut b, pal) = pradera();
1553        b.relief = 0.2;
1554        b.mountains = 0.1;
1555        b.water_level = 0.0;
1556        b.rivers = 0.0;
1557        // Sin fauna: el terreno base. Con fauna densa: aparecen animales encima.
1558        let sin = b.generate_window(7, &pal, dim, [0, 0]);
1559        b.fauna = vec![crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Vaca, densidad: 0.95 }];
1560        let con = b.generate_window(7, &pal, dim, [0, 0]);
1561        let mut diff = 0;
1562        for x in 0..dim[0] {
1563            for y in 0..dim[1] {
1564                for z in 0..dim[2] {
1565                    if sin.get(x, y, z) != con.get(x, y, z) {
1566                        diff += 1;
1567                    }
1568                }
1569            }
1570        }
1571        assert!(diff > 20, "la fauna pobló sólo {diff} vóxels");
1572        // Determinista: misma semilla, misma fauna.
1573        let con2 = b.generate_window(7, &pal, dim, [0, 0]);
1574        assert_eq!(con.get(30, 6, 30), con2.get(30, 6, 30));
1575    }
1576
1577    #[test]
1578    fn los_minerales_afloran_en_la_roca() {
1579        let dim = [64, 48, 64];
1580        let (mut b, mut pal) = pradera();
1581        // Roca sólida hasta arriba, mucho relieve → hay profundidad para minar.
1582        pal.ground = ResolvedMaterial::new(Material::Rock.color(), 0.0);
1583        b.relief = 0.9;
1584        b.mountains = 1.0;
1585        b.rivers = 0.0;
1586        b.water_level = 0.0;
1587        let sin = b.generate_window(5, &pal, dim, [0, 0]);
1588        b.minerales = vec![crate::mineral::MineralUso::with_abundancia(crate::mineral::Mineral::Cobre, 1.0)];
1589        let con = b.generate_window(5, &pal, dim, [0, 0]);
1590        let mut diff = 0;
1591        for x in 0..dim[0] {
1592            for y in 0..dim[1] {
1593                for z in 0..dim[2] {
1594                    if sin.get(x, y, z) != con.get(x, y, z) {
1595                        diff += 1;
1596                    }
1597                }
1598            }
1599        }
1600        assert!(diff > 50, "los minerales cambiaron sólo {diff} vóxels");
1601        // Determinista: misma semilla, misma veta.
1602        let con2 = b.generate_window(5, &pal, dim, [0, 0]);
1603        assert_eq!(con.get(20, 10, 20), con2.get(20, 10, 20));
1604    }
1605
1606    /// La gramática sub-voxel llega a TODO lo estampado: flora (detalle del material),
1607    /// fauna (detalle de ser vivo) y minerales (heredan la roca) — no sólo la
1608    /// superficie del terreno. Antes de este cambio, objetos/seres/vetas quedaban
1609    /// SIEMPRE en el look global (`detail_at == [0,0]`), delatando la caja rectangular.
1610    #[test]
1611    fn el_detalle_por_voxel_llega_a_flora_fauna_y_minerales() {
1612        let dim = [64, 44, 64];
1613        let (mut b, mut pal) = pradera();
1614        b.relief = 0.2;
1615        b.mountains = 0.1;
1616        b.water_level = 0.0;
1617        b.rivers = 0.0;
1618        // Flora con detalle autorado (cactus rugoso+biselado) y fauna densa.
1619        pal.objetos = vec![(
1620            ResolvedMaterial::new(Material::Cactus.color(), 0.0).con_detalle((1.1, 0.6)),
1621            0.9,
1622            Forma::Columnar,
1623        )];
1624        b.objetos = vec![ObjetoUso { material: 9, densidad: 0.9, forma: Forma::Columnar }];
1625        b.fauna = vec![crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Vaca, densidad: 0.95 }];
1626        let g = b.generate_window(7, &pal, dim, [0, 0]);
1627
1628        // Cuenta voxels sólidos con detalle autorado (byte != [0,0]) sobre la piel del
1629        // terreno (por encima de la superficie sólo hay flora/fauna). El terreno base
1630        // de este bioma no tiene detalle → todo lo autorado de arriba es flora/fauna.
1631        let mut autorados = 0;
1632        for x in 0..dim[0] {
1633            for z in 0..dim[2] {
1634                for y in 0..dim[1] {
1635                    let solido = g.get(x, y, z).map(|c| c[3] > 127).unwrap_or(false);
1636                    let con_detalle = g.detail_at(x, y, z) != Some([0, 0]);
1637                    if solido && con_detalle {
1638                        autorados += 1;
1639                    }
1640                }
1641            }
1642        }
1643        assert!(autorados > 30, "flora+fauna dejaron sólo {autorados} voxels con detalle");
1644
1645        // Minerales: heredan el detalle de la roca en que están embebidos. Aislo las
1646        // vetas por DIFF (sin/con mineral, misma semilla): todo voxel que aparece como
1647        // mineral debe llevar el detalle de la roca (`cliff` con rug 0.8) → la veta no
1648        // salta como parche de otra fidelidad. Sin color-matching (el mineral se tiñe).
1649        let mut rocoso = pradera().0;
1650        rocoso.relief = 0.9;
1651        rocoso.mountains = 1.0;
1652        rocoso.rivers = 0.0;
1653        rocoso.water_level = 0.0;
1654        let mut pal_roca = pradera().1;
1655        pal_roca.ground = ResolvedMaterial::new(Material::Rock.color(), 0.0);
1656        pal_roca.cliff = ResolvedMaterial::new(Material::Rock.color(), 0.0).con_detalle((0.8, 0.0));
1657        let sin_min = rocoso.generate_window(5, &pal_roca, dim, [0, 0]);
1658        rocoso.minerales =
1659            vec![crate::mineral::MineralUso::with_abundancia(crate::mineral::Mineral::Cobre, 1.0)];
1660        let con_min = rocoso.generate_window(5, &pal_roca, dim, [0, 0]);
1661        let (mut vetas, mut vetas_con_detalle) = (0, 0);
1662        for x in 0..dim[0] {
1663            for y in 0..dim[1] {
1664                for z in 0..dim[2] {
1665                    // Voxel que CAMBIÓ de color por la veta y sigue sólido = mineral.
1666                    let solido = con_min.get(x, y, z).map(|c| c[3] > 127).unwrap_or(false);
1667                    if con_min.get(x, y, z) != sin_min.get(x, y, z) && solido {
1668                        vetas += 1;
1669                        if con_min.detail_at(x, y, z) != Some([0, 0]) {
1670                            vetas_con_detalle += 1;
1671                        }
1672                    }
1673                }
1674            }
1675        }
1676        assert!(vetas > 20, "casi no hubo veta que testear ({vetas} voxels)");
1677        assert_eq!(vetas, vetas_con_detalle, "toda veta hereda el detalle de la roca");
1678    }
1679
1680    #[test]
1681    fn estampado_es_determinista_por_posicion() {
1682        let (b, _) = pradera();
1683        let dim = [48, 40, 48];
1684        let col = Material::Leaf.color();
1685        let mut g1 = VoxelGrid::new(dim);
1686        let mut g2 = VoxelGrid::new(dim);
1687        b.stamp_flora(&mut g1, Forma::Arbol, col, 24, 4, 24, dim);
1688        b.stamp_flora(&mut g2, Forma::Arbol, col, 24, 4, 24, dim);
1689        assert_eq!(solids(&g1, dim), solids(&g2, dim));
1690        // Distinta posición → distinta silueta (el hash siembra con wx/wz).
1691        let mut g3 = VoxelGrid::new(dim);
1692        b.stamp_flora(&mut g3, Forma::Arbol, col, 12, 4, 30, dim);
1693        // La copa cae en otra región; comparamos el conteo local del centro original.
1694        assert!(solids(&g3, dim) > 0);
1695    }
1696}