tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main llimphi-voxel/src/worldgen.rs Rust · 2114 siq'ikuna · 88.1 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · 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    // --- La vegetación SDF (2026-09-06) ------------------------------------
303    // Al FINAL: `ObjetoUso` viaja serializado y una variante al medio le cambia
304    // la flora a todo mundo guardado. En VOXEL reusan la estampa de un pariente
305    // (`silueta_voxel`); su forma propia la tiene `flora_sdf`.
306    /// Ficus/higuera: copa ancha y baja con raíces aéreas colgando.
307    Ficus,
308    /// Sauce llorón: ramas que caen hasta rozar el suelo.
309    Sauce,
310    /// Jacarandá: copa abierta cubierta de flor violeta.
311    Jacaranda,
312    /// Baobab: tronco tonel y ramas cortas arriba.
313    Baobab,
314    /// Ciprés: columna verde oscura, angosta y alta.
315    Cipres,
316    /// Girasol: tallo alto, disco pardo y corona amarilla.
317    Girasol,
318    /// Margarita: pétalos blancos y centro amarillo.
319    Margarita,
320    /// Tulipán: copa cerrada de tres pétalos sobre tallo liso.
321    Tulipan,
322    /// Rosa: corola de pétalos concéntricos.
323    Rosa,
324    /// Amapola: cuatro pétalos rojos y centro negro.
325    Amapola,
326    /// Orquídea: tallo arqueado con varias flores.
327    Orquidea,
328    /// Agave: roseta de hojas gruesas y puntiagudas.
329    Agave,
330    /// Nopal: palas achatadas apiladas con tunas.
331    Nopal,
332    /// Liana: soga vegetal que cuelga con hojas.
333    Liana,
334}
335
336impl Forma {
337    /// Todas las formas (para ciclar en un editor).
338    pub const ALL: [Forma; 30] = [
339        Forma::Columnar,
340        Forma::Pasto,
341        Forma::Arbusto,
342        Forma::Arbol,
343        Forma::Conifera,
344        Forma::Palmera,
345        Forma::Helecho,
346        Forma::Flor,
347        Forma::Bambu,
348        Forma::Platanera,
349        Forma::Roble,
350        Forma::Hongo,
351        Forma::Tocon,
352        Forma::Trigo,
353        Forma::Junco,
354        Forma::Lavanda,
355        Forma::Ficus,
356        Forma::Sauce,
357        Forma::Jacaranda,
358        Forma::Baobab,
359        Forma::Cipres,
360        Forma::Girasol,
361        Forma::Margarita,
362        Forma::Tulipan,
363        Forma::Rosa,
364        Forma::Amapola,
365        Forma::Orquidea,
366        Forma::Agave,
367        Forma::Nopal,
368        Forma::Liana,
369    ];
370
371    /// **La estampa VOXEL que le toca.** Las 16 primeras tienen la suya; las de la
372    /// vegetación SDF reusan la de un pariente — a la distancia a la que se ve la
373    /// flora esparcida por el bioma, un ficus y un árbol son la misma copa de
374    /// voxels. Su forma propia vive en `flora_sdf`.
375    pub fn silueta_voxel(self) -> Forma {
376        match self {
377            Forma::Ficus => Forma::Arbol,
378            Forma::Sauce => Forma::Arbol,
379            Forma::Jacaranda => Forma::Arbol,
380            Forma::Baobab => Forma::Roble,
381            Forma::Cipres => Forma::Conifera,
382            Forma::Girasol => Forma::Flor,
383            Forma::Margarita => Forma::Flor,
384            Forma::Tulipan => Forma::Flor,
385            Forma::Rosa => Forma::Flor,
386            Forma::Amapola => Forma::Flor,
387            Forma::Orquidea => Forma::Flor,
388            Forma::Agave => Forma::Columnar,
389            Forma::Nopal => Forma::Columnar,
390            Forma::Liana => Forma::Arbusto,
391            otra => otra,
392        }
393    }
394
395    /// Nombre legible (español).
396    pub fn label(self) -> &'static str {
397        match self {
398            Forma::Columnar => "columnar",
399            Forma::Pasto => "grama",
400            Forma::Arbusto => "arbusto",
401            Forma::Arbol => "árbol",
402            Forma::Conifera => "conífera",
403            Forma::Palmera => "palmera",
404            Forma::Helecho => "helecho",
405            Forma::Flor => "flor",
406            Forma::Bambu => "bambú",
407            Forma::Platanera => "platanera",
408            Forma::Roble => "roble",
409            Forma::Hongo => "hongo",
410            Forma::Tocon => "tocón",
411            Forma::Trigo => "trigo",
412            Forma::Junco => "junco",
413            Forma::Lavanda => "lavanda",
414            Forma::Ficus => "ficus",
415            Forma::Sauce => "sauce",
416            Forma::Jacaranda => "jacarandá",
417            Forma::Baobab => "baobab",
418            Forma::Cipres => "ciprés",
419            Forma::Girasol => "girasol",
420            Forma::Margarita => "margarita",
421            Forma::Tulipan => "tulipán",
422            Forma::Rosa => "rosa",
423            Forma::Amapola => "amapola",
424            Forma::Orquidea => "orquídea",
425            Forma::Agave => "agave",
426            Forma::Nopal => "nopal",
427            Forma::Liana => "liana",
428        }
429    }
430
431    /// La forma siguiente (cicla).
432    pub fn next(self) -> Forma {
433        let i = Forma::ALL.iter().position(|&f| f == self).unwrap_or(0);
434        Forma::ALL[(i + 1) % Forma::ALL.len()]
435    }
436}
437
438/// **Material resuelto** para el render: color + grano concretos (ya aplicada la
439/// herencia de [`MaterialDef`](crate::MaterialDef)). Es lo que entra a la paleta.
440#[derive(Debug, Clone, Copy, PartialEq)]
441pub struct ResolvedMaterial {
442    pub color: [u8; 3],
443    pub grain: f32,
444    /// Brillo/reflectividad `[0,1]` (vidrio, acero, cerámica esmaltada, mármol…);
445    /// `0` = mate. Lo lee el ray-march igual que el de los minerales (via el alpha
446    /// del voxel), así que un material lustroso glasea al sol.
447    pub brillo: f32,
448    /// **Detalle sub-voxel** `(rugosidad, bisel)` del material: se hornea POR VOXEL
449    /// en la superficie (via `VoxelGrid::set_detail`), así dos materiales del mismo
450    /// bioma lucen distinto a la vez (rugoso vs liso) — no un look global. `(0,0)` =
451    /// sin detalle (usa el look global del ray-march). Ver `Project::detalle_material`.
452    pub detalle: (f32, f32),
453}
454
455impl ResolvedMaterial {
456    /// Material mate (brillo 0).
457    pub fn new(color: [u8; 3], grain: f32) -> Self {
458        Self { color, grain, brillo: 0.0, detalle: (0.0, 0.0) }
459    }
460
461    /// Material con brillo explícito.
462    pub fn new_b(color: [u8; 3], grain: f32, brillo: f32) -> Self {
463        Self { color, grain, brillo, detalle: (0.0, 0.0) }
464    }
465
466    /// Adjunta el detalle sub-voxel `(rugosidad, bisel)` (builder).
467    pub fn con_detalle(mut self, detalle: (f32, f32)) -> Self {
468        self.detalle = detalle;
469        self
470    }
471}
472
473/// **Uso de un material como objeto** en un bioma: qué material, con qué densidad
474/// `[0,1]` brota, y con qué [`Forma`].
475#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
476pub struct ObjetoUso {
477    pub material: u64,
478    pub densidad: f32,
479    pub forma: Forma,
480}
481
482/// **Uso de un ser** en un bioma: qué ser y con qué probabilidad `[0,1]` puebla.
483/// (Por ahora sólo dato del modelo; el spawn real vendrá con el motor.)
484#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
485pub struct SereUso {
486    pub sere: u64,
487    pub probabilidad: f32,
488}
489
490/// **Paleta resuelta de un bioma**: los colores concretos con los que pintar. La
491/// app la arma resolviendo los ids del bioma contra los materiales del `Project`,
492/// y se la pasa a [`Bioma::generate_window`]. Así worldgen no conoce ids ni herencia.
493#[derive(Debug, Clone)]
494pub struct BiomaPalette {
495    pub ground: ResolvedMaterial,
496    pub cliff: ResolvedMaterial,
497    pub peak: Option<ResolvedMaterial>,
498    /// Color del agua (superficie de ríos/lagos).
499    pub agua: [u8; 3],
500    /// Objetos a esparcir: `(material resuelto, densidad, forma)`.
501    pub objetos: Vec<(ResolvedMaterial, f32, Forma)>,
502}
503
504impl BiomaPalette {
505    /// Paleta de prueba/arranque rápida con un solo material de terreno (sin agua
506    /// teñida especial ni objetos). Útil en tests y fallbacks.
507    pub fn flat(color: [u8; 3], grain: f32) -> Self {
508        let m = ResolvedMaterial::new(color, grain);
509        Self {
510            ground: m,
511            cliff: ResolvedMaterial::new(Material::Rock.color(), Material::Rock.grain()),
512            peak: None,
513            agua: Material::Water.color(),
514            objetos: Vec::new(),
515        }
516    }
517
518    /// Material resuelto de la superficie de la columna en `(wx, wz, y)`: decide
519    /// arena/roca/nieve por altura+pendiente (misma lógica que antes, pero sobre la
520    /// paleta resuelta). `peak_at` viene del bioma.
521    #[allow(clippy::too_many_arguments)]
522    fn surface(
523        &self,
524        peak_at: f32,
525        wx: i32,
526        wz: i32,
527        y: u32,
528        h: u32,
529        slope: f32,
530        dim: [u32; 3],
531        seed: u32,
532    ) -> ResolvedMaterial {
533        let dy = dim[1] as f32;
534        let fh = y as f32 / dy;
535        // Acantilado: la cara superior en pendiente fuerte es roca.
536        if y == h && slope > 2.5 {
537            return self.cliff;
538        }
539        // Cumbre (si la hay).
540        if let Some(pk) = self.peak {
541            if fh > peak_at {
542                return pk;
543            }
544        }
545        // Jitter para que la transición a roca no sea una línea perfecta.
546        let jitter = hash2(wx, wz.wrapping_mul(31).wrapping_add(y as i32), seed ^ 0xABCD) * 0.06 - 0.03;
547        if fh + jitter > 0.72 {
548            return self.cliff;
549        }
550        self.ground
551    }
552}
553
554/// Aplica el **grano de materia** al color base de un vóxel: perturba el brillo
555/// (±`grain`·18%) y mete una pizca de variación por canal, deterministamente por la
556/// posición de mundo. `grain = 0` → color intacto. Función pura → seamless entre
557/// ventanas de streaming.
558fn grained(base: [u8; 3], grain: f32, wx: i32, y: u32, wz: i32, seed: u32) -> [u8; 3] {
559    if grain <= 0.0 {
560        return base;
561    }
562    let g = grain.clamp(0.0, 1.0);
563    let h = hash2(wx, wz.wrapping_mul(31).wrapping_add(y as i32 * 7), seed ^ 0x6817);
564    let bright = 1.0 + (h - 0.5) * 2.0 * g * 0.18; // ±18% a grano pleno
565    let speck = hash2(wx.wrapping_mul(13).wrapping_add(y as i32), wz, seed ^ 0x51A3);
566    let tilt = (speck - 0.5) * 2.0 * g * 12.0; // ±12 niveles, rompe el liso plano
567    let ch = |c: u8, extra: f32| (c as f32 * bright + extra).clamp(0.0, 255.0) as u8;
568    [ch(base[0], tilt), ch(base[1], tilt * 0.7), ch(base[2], -tilt * 0.5)]
569}
570
571/// **Qué sembró el bioma en un punto**: un animal o una planta, con lo que hace
572/// falta para volver a estamparlo igual.
573#[derive(Debug, Clone, Copy, PartialEq)]
574pub enum Sembrada {
575    /// Un animal del catálogo, orientado por `facing` (0=+Z, 1=+X, 2=−Z, 3=−X).
576    Fauna { bicho: crate::bicho::Bicho, facing: u8 },
577    /// Una planta, con el color de follaje que le tocó por material del bioma.
578    Flora { forma: Forma, color: [u8; 3] },
579}
580
581/// **Un ser o una planta ya sembrados** en una ventana: qué, dónde (base local del
582/// estampado) y con qué semilla de mundo. Ver
583/// [`generate_window_con_siembra`](Bioma::generate_window_con_siembra).
584#[derive(Debug, Clone, Copy, PartialEq)]
585pub struct Sembrado {
586    pub clase: Sembrada,
587    /// Base del estampado en coordenadas de la VENTANA (x, y, z).
588    pub base: [u32; 3],
589    /// Columna de MUNDO que lo sembró — la flora la usa para su forma (la misma
590    /// planta en la misma columna sale idéntica en cualquier ventana).
591    pub mundo: [i32; 2],
592    /// Detalle sub-voxel `(rugosidad, bisel)` con que se estampó.
593    pub detalle: (f32, f32),
594}
595
596impl Sembrado {
597    /// Nombre legible de lo sembrado.
598    pub fn label(&self) -> &'static str {
599        match self.clase {
600            Sembrada::Fauna { bicho, .. } => bicho.label(),
601            Sembrada::Flora { forma, .. } => forma.label(),
602        }
603    }
604
605    /// **Altura típica en metros** (= voxels: en este motor el jugador mide 1,7 y
606    /// el voxel es el metro).
607    pub fn alto(&self) -> f32 {
608        match self.clase {
609            Sembrada::Fauna { bicho, .. } => crate::bicho_receta::alto_tipico(bicho),
610            Sembrada::Flora { forma, .. } => crate::flora_sdf::alto_flora(forma),
611        }
612    }
613
614    /// Rumbo en radianes que le tocó (la fauna se siembra en cuartos de vuelta).
615    pub fn yaw(&self) -> f32 {
616        match self.clase {
617            Sembrada::Fauna { facing, .. } => facing as f32 * std::f32::consts::FRAC_PI_2,
618            // La planta no mira a ningún lado, pero girarla rompe la simetría de
619            // que dos de la misma especie se vean calcadas desde el mismo ángulo.
620            Sembrada::Flora { .. } => {
621                (crate::sdf_kit::hash01(self.mundo[0] as u32, self.mundo[1] as u32)) * 6.2831855
622            }
623        }
624    }
625
626    /// **El mismo ser, en la técnica analítica**, posado donde está su estampa.
627    /// `fase` anima (paso del animal, viento en la planta) y `detalle` reparte el
628    /// presupuesto del uniform.
629    pub fn ser_sdf(&self, fase: f32, detalle: crate::DetalleSer) -> llimphi_3d::SdfSer {
630        let pos = [
631            self.base[0] as f32 + 0.5,
632            self.base[1] as f32,
633            self.base[2] as f32 + 0.5,
634        ];
635        match self.clase {
636            Sembrada::Fauna { bicho, .. } => {
637                let andanza = match bicho {
638                    // El que vuela, vuela; el que nada, nada; el resto pasta.
639                    crate::bicho::Bicho::Mariposa
640                    | crate::bicho::Bicho::Abeja
641                    | crate::bicho::Bicho::Libelula
642                    | crate::bicho::Bicho::Luciernaga => crate::Andanza::Volar,
643                    crate::bicho::Bicho::Pez
644                    | crate::bicho::Bicho::Delfin
645                    | crate::bicho::Bicho::Ballena
646                    | crate::bicho::Bicho::Tiburon
647                    | crate::bicho::Bicho::Pulpo => crate::Andanza::Nadar,
648                    _ => {
649                        // Que no pasten todos a la vez: el hash de la columna
650                        // decide quién come, quién camina y quién está alerta.
651                        let h = crate::sdf_kit::hash01(self.mundo[0] as u32 ^ 0x51, self.mundo[1] as u32);
652                        if h < 0.34 {
653                            crate::Andanza::Comer
654                        } else if h < 0.55 {
655                            crate::Andanza::Alerta
656                        } else if h < 0.80 {
657                            crate::Andanza::Quieto
658                        } else {
659                            crate::Andanza::Caminar
660                        }
661                    }
662                };
663                // Cada animal en SU fase: un rebaño en sincro es un desfile.
664                let desfase = crate::sdf_kit::hash01(self.mundo[0] as u32, self.mundo[1] as u32 ^ 0x9A) * 6.28;
665                crate::bicho_sdf::bicho_sdf_lod(bicho, self.alto(), andanza, fase + desfase, detalle)
666                    .posado(pos, self.yaw())
667            }
668            Sembrada::Flora { forma, color } => {
669                let semilla = (self.mundo[0] as u32).wrapping_mul(0x9E37_79B9) ^ (self.mundo[1] as u32);
670                // Viento: la misma racha para todos, con el desfase de su posición
671                // (un bosque que se dobla al unísono se lee como un decorado).
672                let viento = 0.35 * (fase * 0.7 + semilla as f32 * 0.0001).sin();
673                // El follaje del BIOMA manda sobre el propio de la especie: en un
674                // otoño o en una selva, lo que cambia es el verde, no la silueta.
675                let hoja = [
676                    color[0] as f32 / 255.0,
677                    color[1] as f32 / 255.0,
678                    color[2] as f32 / 255.0,
679                ];
680                crate::flora_sdf::flora_sdf_col(forma, self.alto(), semilla, viento, Some(hoja))
681                    .posado(pos, self.yaw())
682            }
683        }
684    }
685}
686
687/// La **receta de relieve de un bioma**: parámetros del terreno + referencias a los
688/// materiales (por id) que lo pintan. La **semilla NO vive aquí** — la aporta el
689/// [`Mundo`](crate::Mundo) al generar (un mismo bioma con semillas distintas da
690/// mundos distintos). Producí el `VoxelGrid` con [`generate_window`](Self::generate_window),
691/// pasándole la [`BiomaPalette`] resuelta.
692#[derive(Debug, Clone, Serialize, Deserialize)]
693pub struct Bioma {
694    /// Id estable (asignado por el [`Project`](crate::Project)).
695    pub id: u64,
696    /// Nombre editable.
697    pub name: String,
698    /// Nivel base del suelo, fracción del alto del mundo `[0,1]` (la llanura).
699    pub base: f32,
700    /// Amplitud de las ondulaciones suaves del suelo (dunas), fracción del alto.
701    pub dune: f32,
702    /// Amplitud de las montañas, fracción del alto.
703    pub relief: f32,
704    /// **Densidad de montañas** `[0,1]`: 0 = casi todo llano, 1 = relieve por todos lados.
705    pub mountains: f32,
706    /// Nivel del agua, fracción del alto `[0,1]`.
707    pub water_level: f32,
708    /// **Densidad de ríos** `[0,1]`.
709    pub rivers: f32,
710    /// Altura normalizada `[0,1]` a partir de la cual aparece la cumbre (`peak`).
711    pub peak_at: f32,
712    /// Material del suelo (id en [`Project::materiales`](crate::Project)).
713    pub ground: u64,
714    /// Material de acantilados/altura (id).
715    pub cliff: u64,
716    /// Material de cumbre por encima de `peak_at` (id); `None` = sin cumbre.
717    pub peak: Option<u64>,
718    /// Objetos a esparcir (flora/props), cada uno con su densidad y forma.
719    #[serde(default)]
720    pub objetos: Vec<ObjetoUso>,
721    /// Minerales embebidos en la roca (vetas, pepitas, estratos…).
722    #[serde(default)]
723    pub minerales: Vec<MineralUso>,
724    /// Casas esparcidas (pueblo/ciudad procedural por semilla).
725    #[serde(default)]
726    pub casas: Vec<crate::casa::CasaUso>,
727    /// **Densidad urbana** `[0,1]`: cuánto del bioma es pueblo. Bajo = aldeas
728    /// aisladas conectadas por rutas; alto = ciudad extendida. Mueve el umbral del
729    /// ruido de manchas de pueblo.
730    #[serde(default = "default_densidad_urbana")]
731    pub densidad_urbana: f32,
732    /// **Fauna** esparcida por semilla: animales voxel (vaca, ciervo, zorro…) que
733    /// pueblan el terreno, como la flora y las casas. Parados sobre el suelo,
734    /// orientados al azar, determinista por posición.
735    #[serde(default)]
736    pub fauna: Vec<crate::bicho::BichoUso>,
737    /// Seres guionados (actores del `Project` por id) para escenas — dato del modelo.
738    #[serde(default)]
739    pub seres: Vec<SereUso>,
740}
741
742fn default_densidad_urbana() -> f32 {
743    0.4
744}
745
746impl Bioma {
747    /// El **planificador urbano** de este bioma para la `seed` y el alto de `dim`:
748    /// retículo de pueblos con la traza limpia y nivelada (plaza, avenidas, cuadras).
749    fn urbe(&self, seed: u32, dim: [u32; 3]) -> crate::urbe::Urbe {
750        crate::urbe::Urbe::new(seed, self.densidad_urbana, dim[1])
751    }
752
753    /// `true` si la columna de mundo `(wx, wz)` cae dentro del **casco urbano** de
754    /// algún pueblo. `densidad_urbana` mueve el radio del casco (aldea ↔ ciudad).
755    /// Delegado al planificador [`crate::urbe`] para que la respuesta coincida
756    /// exactamente con lo que se traza en el terreno.
757    pub fn in_town(&self, seed: u32, wx: i32, wz: i32, dim: [u32; 3]) -> bool {
758        let urbe = self.urbe(seed, dim);
759        let h = |x: i32, z: i32| self.column_height(seed, x, z, dim);
760        urbe.pueblo_en(wx, wz, &h).is_some()
761    }
762
763    /// **Altura del terreno** (índice `y` del voxel sólido superior) en la columna
764    /// de **mundo** `(wx, wz)` para la semilla `seed`. Función pura: mismo punto →
765    /// misma altura en cualquier ventana. Llanura base + dunas + montañas *gated* +
766    /// ríos tallados hacia el agua.
767    pub fn column_height(&self, seed: u32, wx: i32, wz: i32, dim: [u32; 3]) -> u32 {
768        let dy = dim[1] as f32;
769        let scale = world_scale(dim);
770        let (fx, fz) = (wx as f32 * scale, wz as f32 * scale);
771
772        let base = self.base * dy;
773        let dunes = (fbm(fx * 1.7, fz * 1.7, 4, seed ^ 0x11) - 0.5) * 2.0;
774        let dune_h = dunes * self.dune * dy;
775        let c = fbm(fx, fz, 6, seed);
776        let thr = 1.0 - self.mountains.clamp(0.0, 1.0);
777        let m = ((c - thr).max(0.0) / (1.0 - thr).max(1e-3)).clamp(0.0, 1.0);
778        let mtn_h = smooth(m) * self.relief * dy;
779
780        let mut h = base + dune_h + mtn_h;
781
782        if self.rivers > 0.0 {
783            let r = 1.0 - (fbm(fx * 0.8, fz * 0.8, 4, seed ^ 0x77) - 0.5).abs() * 2.0;
784            let width = 0.03 + 0.10 * self.rivers.clamp(0.0, 1.0);
785            if r > 1.0 - width {
786                let t = ((r - (1.0 - width)) / width).clamp(0.0, 1.0);
787                let bed = (self.water_level * dy - 2.0).max(1.0);
788                h += (bed - h) * smooth(t);
789            }
790        }
791
792        (h.clamp(1.0, dy - 1.0)) as u32
793    }
794
795    /// Nivel del agua como índice `y`.
796    #[inline]
797    pub fn water_y(&self, dim: [u32; 3]) -> u32 {
798        (self.water_level * dim[1] as f32) as u32
799    }
800
801    /// `true` si en la columna `(wx, wz)` brota un objeto de densidad `densidad`
802    /// (gate por hash de mundo, sembrado por `salt` para que objetos distintos no se
803    /// pisen). Sólo en suelo seco y llano.
804    fn has_objeto(&self, wx: i32, wz: i32, h: u32, slope: f32, densidad: f32, salt: u32, dim: [u32; 3]) -> bool {
805        if densidad <= 0.0 {
806            return false;
807        }
808        if h <= self.water_y(dim) + 1 || slope > 1.5 {
809            return false;
810        }
811        hash2(wx.wrapping_mul(7), wz.wrapping_mul(13), self.seed_salt(salt)) < densidad
812    }
813
814    /// Mezcla un `salt` con una constante estable para sembrar el gate de objetos.
815    #[inline]
816    fn seed_salt(&self, salt: u32) -> u32 {
817        (0xC4C7u32).wrapping_add(salt.wrapping_mul(0x9E37_79B9))
818    }
819
820    /// **Color de mineral** para el vóxel sólido de mundo `(wx, y, wz)` bajo una
821    /// columna de altura `h`, o `None` si ahí no aflora ningún depósito. La
822    /// profundidad se mide como fracción bajo la superficie; cada mineral sólo
823    /// aparece dentro de su banda `(prof_min, prof_max)`. El primer depósito que
824    /// acierta gana el vóxel.
825    fn mineral_at(&self, wx: i32, y: u32, wz: i32, h: u32, dim: [u32; 3], seed: u32) -> Option<([u8; 3], u8)> {
826        if self.minerales.is_empty() {
827            return None;
828        }
829        let depth = h.saturating_sub(y) as f32 / dim[1] as f32; // 0 en superficie, crece hacia abajo
830        for (i, mu) in self.minerales.iter().enumerate() {
831            if mu.abundancia <= 0.0 || depth < mu.prof_min || depth > mu.prof_max {
832                continue;
833            }
834            let s = seed ^ (0x0E1E_0000u32).wrapping_add((i as u32).wrapping_mul(0x9E37_79B9));
835            if aspecto_hit(mu.aspecto, wx, y, wz, s, mu.abundancia) {
836                // Factores autorables (color/dureza/brillo), no los del enum.
837                let color = mineral_shade(mu.color, mu.dureza, wx, y, wz, s);
838                // El brillo del mineral viaja en el alpha del voxel (lo lee el shader).
839                let alpha = VoxelGrid::shine_to_alpha(mu.brillo);
840                return Some((color, alpha));
841            }
842        }
843        None
844    }
845
846    /// **Construye una ventana del bioma** con la esquina en la columna de mundo
847    /// `origin = [wx, wz]`, semilla `seed` y la [`BiomaPalette`] ya resuelta. Terreno
848    /// + agua + objetos, todo por material. Deja el grid limpio de *dirty*.
849    pub fn generate_window(
850        &self,
851        seed: u32,
852        palette: &BiomaPalette,
853        dim: [u32; 3],
854        origin: [i32; 2],
855    ) -> VoxelGrid {
856        self.generate_window_con_siembra(seed, palette, dim, origin).0
857    }
858
859    /// Igual que [`generate_window`](Self::generate_window) pero además devuelve
860    /// **la siembra**: qué animal y qué planta puso, dónde, y con qué datos.
861    ///
862    /// Existe para que la segunda técnica de graficación pueda entrar al mundo. El
863    /// bioma siembra en VOXELS —que es lo que sirve a cien metros— y quien está
864    /// cerca quiere el ser analítico. Con esta lista, el consumidor
865    /// [`desestampar`](Self::desestampar) los que tiene al lado, los dibuja como
866    /// [`SdfSer`](llimphi_3d::SdfSer) y los vuelve a [`estampar`](Self::estampar)
867    /// cuando se alejan: el mundo no se regenera, sólo cambian de técnica un puñado
868    /// de bricks.
869    ///
870    /// La lista sale de la MISMA pasada que estampa (no de una copia de las
871    /// fórmulas): si se calculara aparte, el día que alguien toque un hash la lista
872    /// y el mundo dejarían de coincidir, y el ser SDF aparecería al lado de su
873    /// propio fantasma de voxels.
874    pub fn generate_window_con_siembra(
875        &self,
876        seed: u32,
877        palette: &BiomaPalette,
878        dim: [u32; 3],
879        origin: [i32; 2],
880    ) -> (VoxelGrid, Vec<Sembrado>) {
881        let mut siembra: Vec<Sembrado> = Vec::new();
882        let [dx, dy, dz] = dim;
883        let mut g = VoxelGrid::new(dim);
884        let (ox, oz) = (origin[0], origin[1]);
885        let water_y = self.water_y(dim);
886
887        let h_at = |lx: i32, lz: i32| self.column_height(seed, ox + lx, oz + lz, dim);
888        // Planificador urbano: traza limpia y NIVELADA (plaza, avenidas, cuadras).
889        let urbe = self.urbe(seed, dim);
890        let hw = |wx: i32, wz: i32| self.column_height(seed, wx, wz, dim);
891
892        for lz in 0..dz as i32 {
893            for lx in 0..dx as i32 {
894                let (wx, wz) = (ox + lx, oz + lz);
895                let natural = h_at(lx, lz);
896                let slope = (natural as i32 - h_at(lx - 1, lz) as i32)
897                    .abs()
898                    .max((natural as i32 - h_at(lx, lz - 1) as i32).abs()) as f32;
899
900                // Consulta urbana: dentro de un casco el suelo se aplana a la cota
901                // del pueblo (o rampa en el borde) → el auto rueda sin escalones.
902                let plano = if !self.casas.is_empty() {
903                    urbe.consultar(wx, wz, &hw)
904                } else {
905                    None
906                };
907                // Cota a construir y cómo pintar la superficie: en el casco es una
908                // explanada nivelada (pendiente 0), en el campo el terreno natural.
909                let (build_h, surf_h, surf_slope, urbano) = match &plano {
910                    Some(p) => (p.suelo.min(dy - 1), p.suelo.min(dy - 1), 0.0, true),
911                    None => (natural.min(dy - 1), natural, slope, false),
912                };
913
914                // Columna sólida (con grano por material). Bajo la piel del terreno,
915                // un depósito de mineral puede reemplazar el color de la roca. En el
916                // casco urbano no afloran minerales (es suelo compactado/explanado).
917                for y in 0..=build_h {
918                    let m = palette.surface(self.peak_at, wx, wz, y, surf_h, surf_slope, dim, seed);
919                    let mineral = if !urbano && y + 1 < surf_h {
920                        self.mineral_at(wx, y, wz, surf_h, dim, seed)
921                    } else {
922                        None
923                    };
924                    match mineral {
925                        // Mineral: color + brillo (alpha) del depósito. Hereda el
926                        // detalle sub-voxel de la ROCA en que está embebido (`cliff`)
927                        // → la veta no salta como un parche de otra fidelidad (misma
928                        // gramática cuantizada = sin valle).
929                        Some((color, alpha)) => {
930                            g.set_rgba(lx as u32, y, lz as u32, color, alpha);
931                            let d = palette.cliff.detalle;
932                            if d != (0.0, 0.0) {
933                                g.set_detail(lx as u32, y, lz as u32, d.0, d.1);
934                            }
935                        }
936                        // Material del terreno: mate, salvo los lustrosos (vidrio,
937                        // acero, mármol…) que llevan su brillo en el alpha.
938                        None => {
939                            let c = grained(m.color, m.grain, wx, y, wz, seed);
940                            if m.brillo > 0.0 {
941                                g.set_rgba(lx as u32, y, lz as u32, c, VoxelGrid::shine_to_alpha(m.brillo));
942                            } else {
943                                g.set(lx as u32, y, lz as u32, c);
944                            }
945                            // Detalle sub-voxel POR VOXEL del material (rugoso/liso, biselado
946                            // o duro): así dos materiales del bioma lucen distinto a la vez.
947                            if m.detalle != (0.0, 0.0) {
948                                g.set_detail(lx as u32, y, lz as u32, m.detalle.0, m.detalle.1);
949                            }
950                        }
951                    }
952                }
953                // Agua (sólo en campo abierto; el casco urbano vive sobre explanada seca).
954                if !urbano && build_h < water_y {
955                    for y in (build_h + 1)..=water_y.min(dy - 1) {
956                        g.set(lx as u32, y, lz as u32, palette.agua);
957                    }
958                }
959                // Pavimento de la traza urbana sobre la explanada nivelada.
960                if let Some(pav) = plano.as_ref().and_then(|p| p.pav) {
961                    g.set(lx as u32, build_h, lz as u32, pav);
962                }
963                // Objetos (flora/props): sólo en campo abierto — el casco urbano
964                // queda despejado salvo el arbolado cívico de la plaza.
965                if !urbano {
966                    for (i, (mat, densidad, forma)) in palette.objetos.iter().enumerate() {
967                        if self.has_objeto(wx, wz, natural, slope, *densidad, i as u32, dim) {
968                            // Detalle sub-voxel POR VOXEL de la flora/prop: su material
969                            // lo lleva igual que el terreno (rugoso/liso, biselado o
970                            // duro) → un cactus rugoso junto a roca lisa, a la vez. El
971                            // pincel tiñe todos los voxels que `place_objeto` estampe.
972                            if mat.detalle != (0.0, 0.0) {
973                                g.set_detail_brush(mat.detalle.0, mat.detalle.1);
974                            }
975                            self.place_objeto(&mut g, *forma, mat.color, lx as u32, natural + 1, lz as u32, wx, wz, dim);
976                            g.clear_detail_brush();
977                            siembra.push(Sembrado {
978                                clase: Sembrada::Flora { forma: *forma, color: mat.color },
979                                base: [lx as u32, natural + 1, lz as u32],
980                                mundo: [wx, wz],
981                                detalle: mat.detalle,
982                            });
983                            break; // un objeto por columna
984                        }
985                    }
986                }
987            }
988        }
989
990        // Casas y mobiliario cívico: una vez por pueblo, todo asentado sobre la
991        // explanada nivelada del casco (misma cota `suelo` que la traza) → nada de
992        // casas escalonadas. Iglesia al norte de la plaza, fuente/bancos/árboles en
993        // la plaza, y una casa por CUADRA (interior de manzana), del estilo del bioma.
994        if !self.casas.is_empty() {
995            let interior = urbe.radio_interior();
996            for p in crate::urbe::pueblos_en_ventana(&urbe, [ox, oz], dim, &hw) {
997                let [cx, cz] = p.centro;
998                let base = p.suelo; // cota nivelada del casco
999                let en_ventana = |wx: i32, wz: i32| -> Option<(u32, u32)> {
1000                    let (lx, lz) = (wx - ox, wz - oz);
1001                    if lx >= 0 && lz >= 0 && lx < dx as i32 && lz < dz as i32 {
1002                        Some((lx as u32, lz as u32))
1003                    } else {
1004                        None
1005                    }
1006                };
1007
1008                // Iglesia: sobre su plot, al norte de la plaza, mirando al sur.
1009                if let Some((lx, lz)) = en_ventana(cx, cz + crate::urbe::IGLESIA_V) {
1010                    crate::stamp_mueble(&mut g, crate::Mueble::Iglesia, lx, base + 1, lz, 0);
1011                }
1012                // Fuente al centro de la plaza + bancos + arbolado en las esquinas.
1013                if let Some((lx, lz)) = en_ventana(cx, cz) {
1014                    crate::stamp_mueble(&mut g, crate::Mueble::Fuente, lx, base + 1, lz, 0);
1015                }
1016                for (bi, &(bx, bz)) in [(-4i32, 0i32), (4, 0), (0, 4)].iter().enumerate() {
1017                    if let Some((lx, lz)) = en_ventana(cx + bx, cz + bz) {
1018                        crate::stamp_mueble(&mut g, crate::Mueble::Banco, lx, base + 1, lz, bi as u8);
1019                    }
1020                }
1021                let ph = crate::urbe::PLAZA_HALF;
1022                for &(tx, tz) in &[(-ph, -ph), (ph, -ph), (-ph, ph), (ph, ph)] {
1023                    if let Some((lx, lz)) = en_ventana(cx + tx, cz + tz) {
1024                        crate::stamp_mueble(&mut g, crate::Mueble::Ficus, lx, base + 1, lz, 0);
1025                    }
1026                }
1027
1028                // Una casa por cuadra: centro de manzana (offset medio paso desde las
1029                // líneas de la grilla) dentro del radio interior, salteando plaza,
1030                // iglesia y calzada.
1031                let paso = crate::urbe::PASO_CUADRA;
1032                let n = interior / paso + 1;
1033                for ku in -n..=n {
1034                    for kv in -n..=n {
1035                        let bu = ku * paso + paso / 2; // centro de cuadra (local)
1036                        let bv = kv * paso + paso / 2;
1037                        if bu * bu + bv * bv > interior * interior {
1038                            continue; // fuera del casco urbanizable
1039                        }
1040                        if urbe.zona(bu, bv) != crate::urbe::Zona::Parcela {
1041                            continue; // cayó en plaza/calzada
1042                        }
1043                        let (cwx, cwz) = (cx + bu, cz + bv);
1044                        let (lx, lz) = match en_ventana(cwx, cwz) {
1045                            Some(v) => v,
1046                            None => continue,
1047                        };
1048                        for (i, cu) in self.casas.iter().enumerate() {
1049                            if cu.densidad <= 0.0 {
1050                                continue;
1051                            }
1052                            let gate = hash2(cwx.wrapping_mul(13), cwz.wrapping_mul(7), p.seed ^ (0x50 + i as u32));
1053                            if gate < cu.densidad {
1054                                let cseed = crate::casa::casa_seed(seed, cwx, cwz);
1055                                crate::casa::stamp_casa(&mut g, cu.tipo, lx, base + 1, lz, cseed);
1056                                break; // una casa por cuadra
1057                            }
1058                        }
1059                    }
1060                }
1061            }
1062        }
1063
1064        // Fauna: animales esparcidos por semilla en una grilla propia (más fina que
1065        // la de casas). Cada parcela con acierto pone un ser parado sobre el suelo,
1066        // orientado por la posición. Los seres viven en el campo, no en la calzada.
1067        if !self.fauna.is_empty() {
1068            let plot = 11i32;
1069            let ix0 = ox.div_euclid(plot);
1070            let ix1 = (ox + dx as i32).div_euclid(plot);
1071            let iz0 = oz.div_euclid(plot);
1072            let iz1 = (oz + dz as i32).div_euclid(plot);
1073            for ix in ix0..=ix1 {
1074                for iz in iz0..=iz1 {
1075                    // Jitter del punto dentro de la parcela → rebaño no cuadriculado.
1076                    let jx = (hash2(ix.wrapping_mul(5), iz.wrapping_mul(9), self.seed_salt(0x71) ^ seed) * plot as f32) as i32;
1077                    let jz = (hash2(ix.wrapping_mul(11), iz.wrapping_mul(3), self.seed_salt(0x72) ^ seed) * plot as f32) as i32;
1078                    let cwx = ix * plot + jx;
1079                    let cwz = iz * plot + jz;
1080                    let (lx, lz) = (cwx - ox, cwz - oz);
1081                    if lx < 0 || lz < 0 || lx >= dx as i32 || lz >= dz as i32 {
1082                        continue;
1083                    }
1084                    let hh = self.column_height(seed, cwx, cwz, dim);
1085                    if hh <= water_y + 1 || hh >= dy {
1086                        continue; // agua/muy bajo
1087                    }
1088                    // Sin fauna sobre la calzada (rutas/calles) ni en pendiente fuerte.
1089                    let slope = (hh as i32 - self.column_height(seed, cwx - 1, cwz, dim) as i32)
1090                        .abs()
1091                        .max((hh as i32 - self.column_height(seed, cwx, cwz - 1, dim) as i32).abs());
1092                    if slope > 2 {
1093                        continue;
1094                    }
1095                    let ruta = !self.casas.is_empty()
1096                        && (cwx.rem_euclid(48) < 5 || cwz.rem_euclid(48) < 5);
1097                    if ruta {
1098                        continue;
1099                    }
1100                    let facing = (hash2(cwx, cwz, self.seed_salt(0x7F) ^ seed) * 4.0) as u8 & 3;
1101                    for (i, fu) in self.fauna.iter().enumerate() {
1102                        if fu.densidad <= 0.0 {
1103                            continue;
1104                        }
1105                        let gate = hash2(cwx.wrapping_mul(17), cwz.wrapping_mul(11), self.seed_salt(0x70 + i as u32) ^ seed);
1106                        if gate < fu.densidad {
1107                            // Un ser vivo lleva su gramática sub-voxel (redondeo orgánico
1108                            // + grano de piel) → deja de ser una caja rectangular. El
1109                            // pincel tiñe todo el cuerpo que `stamp_bicho` estampe.
1110                            let (r, b) = crate::bicho::DETALLE_SER_VIVO;
1111                            g.set_detail_brush(r, b);
1112                            crate::bicho::stamp_bicho(&mut g, fu.bicho, lx as u32, hh + 1, lz as u32, facing);
1113                            g.clear_detail_brush();
1114                            siembra.push(Sembrado {
1115                                clase: Sembrada::Fauna { bicho: fu.bicho, facing },
1116                                base: [lx as u32, hh + 1, lz as u32],
1117                                mundo: [cwx, cwz],
1118                                detalle: crate::bicho::DETALLE_SER_VIVO,
1119                            });
1120                            break; // un ser por parcela
1121                        }
1122                    }
1123                }
1124            }
1125        }
1126
1127        g.reset_dirty();
1128        (g, siembra)
1129    }
1130
1131    /// **Estampa** un sembrado en el grid (idempotente: los mismos datos dan los
1132    /// mismos voxels, que es lo que deja re-plantar al que se alejó).
1133    pub fn estampar(&self, g: &mut VoxelGrid, s: &Sembrado, dim: [u32; 3]) {
1134        let [x, y, z] = s.base;
1135        if s.detalle != (0.0, 0.0) {
1136            g.set_detail_brush(s.detalle.0, s.detalle.1);
1137        }
1138        match s.clase {
1139            Sembrada::Fauna { bicho, facing } => crate::bicho::stamp_bicho(g, bicho, x, y, z, facing),
1140            Sembrada::Flora { forma, color } => {
1141                self.place_objeto(g, forma, color, x, y, z, s.mundo[0], s.mundo[1], dim)
1142            }
1143        }
1144        g.clear_detail_brush();
1145    }
1146
1147    /// **Borra** de `g` exactamente los voxels que este sembrado escribió.
1148    ///
1149    /// «Exactamente» no es una figura: la huella se calcula ESTAMPANDO en una
1150    /// grilla de bolsillo y leyendo qué celdas quedaron ocupadas. Borrar una caja
1151    /// a ojo se llevaría por delante la pared de la casa de atrás o el árbol
1152    /// vecino — y eso es un agujero en el mundo que aparece recién cuando alguien
1153    /// camina hasta ahí.
1154    pub fn desestampar(&self, g: &mut VoxelGrid, s: &Sembrado, dim: [u32; 3]) {
1155        for [ox, oy, oz] in self.huella(s) {
1156            let (x, y, z) = (
1157                s.base[0] as i32 + ox,
1158                s.base[1] as i32 + oy,
1159                s.base[2] as i32 + oz,
1160            );
1161            if x >= 0 && y >= 0 && z >= 0 && (x as u32) < dim[0] && (y as u32) < dim[1] && (z as u32) < dim[2] {
1162                g.clear(x as u32, y as u32, z as u32);
1163            }
1164        }
1165    }
1166
1167    /// La huella de un sembrado: offsets `(dx, dy, dz)` respecto de su base que el
1168    /// estampado ocupa. Se obtiene estampándolo en una grilla chica.
1169    fn huella(&self, s: &Sembrado) -> Vec<[i32; 3]> {
1170        const L: u32 = 40; // lado de la grilla de bolsillo (la flora más alta cabe)
1171        const C: u32 = L / 2;
1172        let dim = [L, L, L];
1173        let mut chica = VoxelGrid::new(dim);
1174        let base = Sembrado { base: [C, 2, C], ..*s };
1175        self.estampar(&mut chica, &base, dim);
1176        let mut out = Vec::new();
1177        for z in 0..L {
1178            for y in 0..L {
1179                for x in 0..L {
1180                    if chica.get(x, y, z).map(|c| c[3] != 0).unwrap_or(false) {
1181                        out.push([x as i32 - C as i32, y as i32 - 2, z as i32 - C as i32]);
1182                    }
1183                }
1184            }
1185        }
1186        out
1187    }
1188
1189    /// Estampa una **sola** forma de vegetación con follaje `col` y base en
1190    /// `(x, base_y, z)` (local), determinista por esa posición. Pensado para
1191    /// catálogos/tests; la generación normal usa [`generate_window`](Self::generate_window).
1192    pub fn stamp_flora(
1193        &self,
1194        g: &mut VoxelGrid,
1195        forma: Forma,
1196        col: [u8; 3],
1197        x: u32,
1198        base_y: u32,
1199        z: u32,
1200        dim: [u32; 3],
1201    ) {
1202        self.place_objeto(g, forma, col, x, base_y, z, x as i32, z as i32, dim);
1203    }
1204
1205    /// Coloca un objeto de vegetación con la base en `(x, base_y, z)` (local),
1206    /// despachando por [`Forma`]. `col` es el **color de follaje** (del material del
1207    /// objeto); los troncos leñosos usan [`WOOD`]. Todo es determinista por la
1208    /// posición de mundo `(wx, wz)` → seamless entre ventanas de streaming. Los
1209    /// vóxels fuera de la ventana se recortan (igual que el cactus histórico).
1210    #[allow(clippy::too_many_arguments)]
1211    fn place_objeto(
1212        &self,
1213        g: &mut VoxelGrid,
1214        forma: Forma,
1215        col: [u8; 3],
1216        x: u32,
1217        base_y: u32,
1218        z: u32,
1219        wx: i32,
1220        wz: i32,
1221        dim: [u32; 3],
1222    ) {
1223        // La vegetación SDF cae en la estampa de su pariente voxel (ver
1224        // `Forma::silueta_voxel`); lo fija el test `cada_forma_estampa_voxels`.
1225        let forma = forma.silueta_voxel();
1226        let cx = x as i32;
1227        let cz = z as i32;
1228        let by = base_y as i32;
1229        // Follaje con jitter de brillo por vóxel → copas menos «plásticas».
1230        let leaf = |xi: i32, yi: i32, zi: i32| leaf_tint(col, wx + (xi - cx), yi as u32, wz + (zi - cz), 0x1EAF);
1231
1232        match forma {
1233            Forma::Columnar => {
1234                let th = 3 + (hash2(wx, wz, 0x1357) * 4.0) as i32;
1235                for k in 0..th {
1236                    put(g, dim, cx, by + k, cz, col);
1237                }
1238                let arm_seed = hash2(wx.wrapping_add(1), wz, 0x2468);
1239                if th >= 4 && arm_seed > 0.45 {
1240                    let ay = by + th / 2;
1241                    put(g, dim, cx + 1, ay, cz, col);
1242                    put(g, dim, cx + 1, ay + 1, cz, col);
1243                    if arm_seed > 0.7 {
1244                        put(g, dim, cx - 1, ay + 1, cz, col);
1245                        put(g, dim, cx - 1, ay + 2, cz, col);
1246                    }
1247                }
1248            }
1249
1250            Forma::Pasto => {
1251                let n = 3 + (hash2(wx, wz, 0x9E13) * 4.0) as i32; // 3..6 matas
1252                for i in 0..n {
1253                    let ox = (hash2(wx, wz.wrapping_add(i), 0x31) * 3.0) as i32 - 1; // -1..1
1254                    let oz = (hash2(wx.wrapping_add(7), wz.wrapping_add(i), 0x59) * 3.0) as i32 - 1;
1255                    let bh = 1 + (hash2(wx.wrapping_add(i), wz, 0x77) * 2.0) as i32; // 1..2
1256                    for k in 0..bh {
1257                        put(g, dim, cx + ox, by + k, cz + oz, leaf(cx + ox, by + k, cz + oz));
1258                    }
1259                }
1260            }
1261
1262            Forma::Arbusto => {
1263                let r = 2i32;
1264                let cy = by + 2;
1265                for yy in by..=by + 3 {
1266                    for xx in -r..=r {
1267                        for zz in -r..=r {
1268                            let d2 = xx * xx + (yy - cy) * (yy - cy) + zz * zz;
1269                            if d2 <= r * r + 1
1270                                && hash2(wx + xx, wz.wrapping_add(zz).wrapping_add(yy), 0x2B) > 0.18
1271                            {
1272                                put(g, dim, cx + xx, yy, cz + zz, leaf(cx + xx, yy, cz + zz));
1273                            }
1274                        }
1275                    }
1276                }
1277            }
1278
1279            Forma::Arbol => {
1280                let th = 4 + (hash2(wx, wz, 0x1357) * 5.0) as i32; // 4..8 tronco
1281                for k in 0..th {
1282                    put(g, dim, cx, by + k, cz, WOOD);
1283                }
1284                let cy = by + th; // centro de copa en la cima del tronco
1285                let r = 2 + (hash2(wx.wrapping_add(3), wz, 0x246) * 2.0) as i32; // 2..3
1286                for xx in -r..=r {
1287                    for yy in -r..=r {
1288                        for zz in -r..=r {
1289                            let d2 = xx * xx + yy * yy + zz * zz;
1290                            if d2 <= r * r + r
1291                                && hash2(wx + xx, wz.wrapping_add(zz).wrapping_add(yy), 0x9C) > 0.12
1292                            {
1293                                put(g, dim, cx + xx, cy + yy, cz + zz, leaf(cx + xx, cy + yy, cz + zz));
1294                            }
1295                        }
1296                    }
1297                }
1298            }
1299
1300            Forma::Conifera => {
1301                let h = 6 + (hash2(wx, wz, 0x1357) * 5.0) as i32; // 6..10 de follaje
1302                put(g, dim, cx, by, cz, WOOD); // tronquito visible
1303                let maxr = 3i32;
1304                for layer in 0..h {
1305                    let yy = by + 1 + layer;
1306                    let r = maxr * (h - layer) / h; // decrece hacia arriba
1307                    for xx in -r..=r {
1308                        for zz in -r..=r {
1309                            if xx * xx + zz * zz <= r * r + 1
1310                                && hash2(wx + xx, wz.wrapping_add(zz).wrapping_add(yy), 0x5C1) > 0.10
1311                            {
1312                                put(g, dim, cx + xx, yy, cz + zz, leaf(cx + xx, yy, cz + zz));
1313                            }
1314                        }
1315                    }
1316                }
1317                put(g, dim, cx, by + 1 + h, cz, leaf(cx, by + 1 + h, cz)); // punta
1318            }
1319
1320            Forma::Palmera => {
1321                let h = 7 + (hash2(wx, wz, 0x1357) * 6.0) as i32; // 7..12 tronco
1322                let lean = if hash2(wx, wz, 0x9) > 0.5 { 1 } else { -1 };
1323                let mut tx = cx;
1324                for k in 0..h {
1325                    if k == h * 2 / 3 {
1326                        tx += lean; // leve curva cerca de la corona
1327                    }
1328                    put(g, dim, tx, by + k, cz, WOOD);
1329                }
1330                let ty = by + h;
1331                put(g, dim, tx, ty, cz, leaf(tx, ty, cz)); // corona
1332                // Frondas en 8 rumbos: suben 1 y luego caen (arco).
1333                let dirs = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)];
1334                for (dxi, dzi) in dirs {
1335                    let len = 3 + (hash2(tx + dxi, cz + dzi, 0x33) * 2.0) as i32; // 3..4
1336                    for s in 1..=len {
1337                        let fx = tx + dxi * s;
1338                        let fz = cz + dzi * s;
1339                        let fy = ty + 1 - s; // arco: s=1→ty, luego baja
1340                        put(g, dim, fx, fy, fz, leaf(fx, fy, fz));
1341                    }
1342                }
1343            }
1344
1345            Forma::Helecho => {
1346                let sh = 2i32;
1347                for k in 0..sh {
1348                    put(g, dim, cx, by + k, cz, leaf(cx, by + k, cz));
1349                }
1350                let ty = by + sh;
1351                let dirs = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, -1)];
1352                for (dxi, dzi) in dirs {
1353                    let len = 2 + (hash2(wx + dxi, wz + dzi, 0x71) * 2.0) as i32; // 2..3
1354                    for s in 1..=len {
1355                        let fy = (ty - s / 2).max(by); // frondas que arquean hacia abajo
1356                        put(g, dim, cx + dxi * s, fy, cz + dzi * s, leaf(cx + dxi * s, fy, cz + dzi * s));
1357                    }
1358                }
1359            }
1360
1361            Forma::Flor => {
1362                let sh = 2 + (hash2(wx, wz, 0x5) * 2.0) as i32; // 2..3 tallo
1363                for k in 0..sh {
1364                    put(g, dim, cx, by + k, cz, STEM);
1365                }
1366                let ty = by + sh;
1367                put(g, dim, cx, ty, cz, col); // corola (color del material)
1368                for (dxi, dzi) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
1369                    put(g, dim, cx + dxi, ty, cz + dzi, col); // pétalos en cruz
1370                }
1371            }
1372
1373            Forma::Bambu => {
1374                let n = 3 + (hash2(wx, wz, 0xB00) * 3.0) as i32; // 3..5 cañas
1375                for i in 0..n {
1376                    let ox = (hash2(wx.wrapping_add(i), wz, 0xB01) * 3.0) as i32 - 1;
1377                    let oz = (hash2(wx, wz.wrapping_add(i), 0xB02) * 3.0) as i32 - 1;
1378                    let ch = 7 + (hash2(wx.wrapping_add(i * 3), wz, 0xB03) * 6.0) as i32; // 7..12
1379                    for k in 0..ch {
1380                        put(g, dim, cx + ox, by + k, cz + oz, leaf(cx + ox, by + k, cz + oz));
1381                    }
1382                    // Hojitas en la punta.
1383                    put(g, dim, cx + ox + 1, by + ch - 1, cz + oz, leaf(cx + ox + 1, by + ch - 1, cz + oz));
1384                    put(g, dim, cx + ox, by + ch, cz + oz + 1, leaf(cx + ox, by + ch, cz + oz + 1));
1385                }
1386            }
1387
1388            Forma::Platanera => {
1389                let h = 3 + (hash2(wx, wz, 0x9A0) * 2.0) as i32; // 3..4 pseudo-tronco verde
1390                for k in 0..h {
1391                    put(g, dim, cx, by + k, cz, leaf(cx, by + k, cz));
1392                }
1393                let ty = by + h;
1394                // Hojas anchas (2 de ancho) que suben 1 y caen despacio.
1395                let dirs = [(1i32, 0i32), (-1, 0), (0, 1), (0, -1), (1, 1)];
1396                for (dxi, dzi) in dirs {
1397                    let len = 4 + (hash2(cx + dxi, cz + dzi, 0x9A1) * 2.0) as i32; // 4..5
1398                    let (px, pz) = (-dzi, dxi); // perpendicular → ancho de hoja
1399                    for s in 1..=len {
1400                        let fy = ty + 1 - (s + 1) / 2;
1401                        let (bx, bz) = (cx + dxi * s, cz + dzi * s);
1402                        put(g, dim, bx, fy, bz, leaf(bx, fy, bz));
1403                        if s >= 2 {
1404                            put(g, dim, bx + px, fy, bz + pz, leaf(bx + px, fy, bz + pz));
1405                        }
1406                    }
1407                }
1408            }
1409
1410            Forma::Roble => {
1411                let th = 5 + (hash2(wx, wz, 0x0A0) * 4.0) as i32; // 5..8 tronco grueso
1412                for k in 0..th {
1413                    put(g, dim, cx, by + k, cz, WOOD);
1414                    if k < th * 2 / 3 {
1415                        put(g, dim, cx + 1, by + k, cz, WOOD);
1416                        put(g, dim, cx, by + k, cz + 1, WOOD);
1417                        put(g, dim, cx + 1, by + k, cz + 1, WOOD);
1418                    }
1419                }
1420                let cy = by + th + 1;
1421                let r = 4i32; // copa amplia, achatada
1422                for xx in -r..=r {
1423                    for yy in -3..=3 {
1424                        for zz in -r..=r {
1425                            let d2 = xx * xx + yy * yy * 2 + zz * zz;
1426                            if d2 <= r * r + 2
1427                                && hash2(wx + xx, wz.wrapping_add(zz).wrapping_add(yy), 0x0A1) > 0.14
1428                            {
1429                                put(g, dim, cx + xx, cy + yy, cz + zz, leaf(cx + xx, cy + yy, cz + zz));
1430                            }
1431                        }
1432                    }
1433                }
1434            }
1435
1436            Forma::Hongo => {
1437                let sh = 1 + (hash2(wx, wz, 0xF00) * 2.0) as i32; // 1..2 pie
1438                for k in 0..sh {
1439                    put(g, dim, cx, by + k, cz, MUSH_STEM);
1440                }
1441                let ty = by + sh;
1442                let r = 1 + (hash2(wx, wz, 0xF01) * 1.5) as i32; // sombrero radio 1..2
1443                for xx in -r..=r {
1444                    for zz in -r..=r {
1445                        if xx * xx + zz * zz <= r * r + 1 {
1446                            put(g, dim, cx + xx, ty, cz + zz, FUNGUS_CAP);
1447                        }
1448                    }
1449                }
1450                put(g, dim, cx, ty + 1, cz, FUNGUS_CAP); // domo
1451            }
1452
1453            Forma::Tocon => {
1454                let h = 2i32; // cepa baja y gruesa
1455                for yy in 0..h {
1456                    for xx in -1..=1 {
1457                        for zz in -1..=1 {
1458                            if xx * xx + zz * zz <= 2 {
1459                                put(g, dim, cx + xx, by + yy, cz + zz, WOOD);
1460                            }
1461                        }
1462                    }
1463                }
1464                // Musgo en la cara superior.
1465                for xx in -1..=1 {
1466                    for zz in -1..=1 {
1467                        if xx * xx + zz * zz <= 2
1468                            && hash2(wx + xx, wz.wrapping_add(zz), 0x70C) > 0.4
1469                        {
1470                            put(g, dim, cx + xx, by + h, cz + zz, leaf(cx + xx, by + h, cz + zz));
1471                        }
1472                    }
1473                }
1474            }
1475
1476            Forma::Trigo => {
1477                let n = 4 + (hash2(wx, wz, 0x7A0) * 4.0) as i32; // 4..7 espigas
1478                for i in 0..n {
1479                    let ox = (hash2(wx.wrapping_add(i), wz, 0x7A1) * 3.0) as i32 - 1;
1480                    let oz = (hash2(wx, wz.wrapping_add(i), 0x7A2) * 3.0) as i32 - 1;
1481                    let sh = 3 + (hash2(wx.wrapping_add(i * 2), wz, 0x7A3) * 2.0) as i32; // 3..4
1482                    for k in 0..sh {
1483                        put(g, dim, cx + ox, by + k, cz + oz, leaf(cx + ox, by + k, cz + oz));
1484                    }
1485                    put(g, dim, cx + ox, by + sh, cz + oz, col); // espiga (grano dorado)
1486                }
1487            }
1488
1489            Forma::Junco => {
1490                let n = 4 + (hash2(wx, wz, 0x5A0) * 4.0) as i32; // 4..7 cañas
1491                for i in 0..n {
1492                    let ox = (hash2(wx.wrapping_add(i), wz, 0x5A1) * 3.0) as i32 - 1;
1493                    let oz = (hash2(wx, wz.wrapping_add(i), 0x5A2) * 3.0) as i32 - 1;
1494                    let sh = 4 + (hash2(wx.wrapping_add(i * 2), wz, 0x5A3) * 3.0) as i32; // 4..6
1495                    for k in 0..sh {
1496                        put(g, dim, cx + ox, by + k, cz + oz, leaf(cx + ox, by + k, cz + oz));
1497                    }
1498                    // Espadaña marrón (cattail) en el tope.
1499                    put(g, dim, cx + ox, by + sh, cz + oz, CATTAIL);
1500                    put(g, dim, cx + ox, by + sh - 1, cz + oz, CATTAIL);
1501                }
1502            }
1503
1504            Forma::Lavanda => {
1505                let r = 1i32; // matojo bajo verde
1506                for xx in -r..=r {
1507                    for zz in -r..=r {
1508                        put(g, dim, cx + xx, by, cz + zz, STEM);
1509                        // Espigas florales en tresbolillo.
1510                        if (xx + zz).rem_euclid(2) == 0 {
1511                            put(g, dim, cx + xx, by + 1, cz + zz, col);
1512                            if hash2(wx + xx, wz.wrapping_add(zz), 0x1A7) > 0.5 {
1513                                put(g, dim, cx + xx, by + 2, cz + zz, col);
1514                            }
1515                        }
1516                    }
1517                }
1518            }
1519            // Inalcanzable: `Forma::silueta_voxel` (primera línea de esta función)
1520            // remapea toda forma sin estampa propia. Lo fija el test
1521            // `cada_forma_estampa_voxels`.
1522            _ => {}
1523        }
1524    }
1525}
1526
1527/// Color de tronco/madera para las formas leñosas (árbol, palmera, conífera).
1528const WOOD: [u8; 3] = [102, 72, 44];
1529
1530/// Color de tallo verde para las flores (independiente del color de la corola).
1531const STEM: [u8; 3] = [78, 130, 60];
1532
1533/// Pie pálido del hongo.
1534const MUSH_STEM: [u8; 3] = [216, 206, 182];
1535
1536/// Sombrero del hongo (rojo clásico) — color intrínseco de la forma.
1537const FUNGUS_CAP: [u8; 3] = [176, 58, 48];
1538
1539/// Espadaña marrón del junco (cattail).
1540const CATTAIL: [u8; 3] = [122, 82, 46];
1541
1542/// Escribe un vóxel en coordenadas locales con signo, recortando fuera de la ventana.
1543#[inline]
1544fn put(g: &mut VoxelGrid, dim: [u32; 3], xi: i32, yi: i32, zi: i32, c: [u8; 3]) {
1545    if xi >= 0
1546        && yi >= 0
1547        && zi >= 0
1548        && (xi as u32) < dim[0]
1549        && (yi as u32) < dim[1]
1550        && (zi as u32) < dim[2]
1551    {
1552        g.set(xi as u32, yi as u32, zi as u32, c);
1553    }
1554}
1555
1556/// Perturba un color de follaje ±15% de brillo por vóxel, determinista por la
1557/// posición de mundo → copas con variación de hoja en vez de un verde plano.
1558fn leaf_tint(base: [u8; 3], wx: i32, y: u32, wz: i32, seed: u32) -> [u8; 3] {
1559    let h = hash2(wx.wrapping_mul(9).wrapping_add(y as i32), wz.wrapping_mul(5), seed ^ 0x7A31);
1560    let f = 0.85 + h * 0.30; // 0.85..1.15
1561    let ch = |c: u8| (c as f32 * f).clamp(0.0, 255.0) as u8;
1562    [ch(base[0]), ch(base[1]), ch(base[2])]
1563}
1564
1565#[cfg(test)]
1566mod tests {
1567    use super::*;
1568
1569    /// Un bioma de desierto de prueba (ids ficticios; la generación no los usa, usa
1570    /// la paleta) + su paleta de arena/roca.
1571    fn desierto() -> (Bioma, BiomaPalette) {
1572        let b = Bioma {
1573            id: 1,
1574            name: "desierto".into(),
1575            base: 0.30,
1576            dune: 0.05,
1577            relief: 0.45,
1578            mountains: 0.12,
1579            water_level: 0.26,
1580            rivers: 0.18,
1581            peak_at: 1.0,
1582            ground: 1,
1583            cliff: 2,
1584            peak: None,
1585            objetos: vec![ObjetoUso { material: 3, densidad: 0.010, forma: Forma::Columnar }],
1586            minerales: vec![],
1587            densidad_urbana: 0.4,
1588            casas: Vec::new(),
1589            fauna: vec![],
1590            seres: vec![],
1591        };
1592        let pal = BiomaPalette {
1593            ground: ResolvedMaterial::new(Material::Sand.color(), Material::Sand.grain()),
1594            cliff: ResolvedMaterial::new(Material::Rock.color(), Material::Rock.grain()),
1595            peak: None,
1596            agua: Material::Water.color(),
1597            objetos: vec![(ResolvedMaterial::new(Material::Cactus.color(), 0.0), 0.010, Forma::Columnar)],
1598        };
1599        (b, pal)
1600    }
1601
1602    fn pradera() -> (Bioma, BiomaPalette) {
1603        let b = Bioma {
1604            id: 2,
1605            name: "pradera".into(),
1606            base: 0.22,
1607            dune: 0.10,
1608            relief: 0.7,
1609            mountains: 0.5,
1610            water_level: 0.30,
1611            rivers: 0.25,
1612            peak_at: 0.80,
1613            ground: 4,
1614            cliff: 2,
1615            peak: Some(5),
1616            objetos: vec![],
1617            minerales: vec![],
1618            densidad_urbana: 0.4,
1619            casas: Vec::new(),
1620            fauna: vec![],
1621            seres: vec![],
1622        };
1623        let pal = BiomaPalette {
1624            ground: ResolvedMaterial::new(Material::Grass.color(), Material::Grass.grain()),
1625            cliff: ResolvedMaterial::new(Material::Rock.color(), Material::Rock.grain()),
1626            peak: Some(ResolvedMaterial::new(Material::Snow.color(), Material::Snow.grain())),
1627            agua: Material::Water.color(),
1628            objetos: vec![],
1629        };
1630        (b, pal)
1631    }
1632
1633    #[test]
1634    fn altura_es_funcion_pura_de_mundo() {
1635        let (b, _) = desierto();
1636        let dim = [96, 48, 96];
1637        assert_eq!(b.column_height(7, 1234, -567, dim), b.column_height(7, 1234, -567, dim));
1638    }
1639
1640    #[test]
1641    fn desierto_es_mas_llano_que_pradera() {
1642        let dim = [128, 56, 128];
1643        let var = |b: &Bioma| {
1644            let mut hs = Vec::new();
1645            for z in (0..400).step_by(7) {
1646                for x in (0..400).step_by(7) {
1647                    hs.push(b.column_height(42, x, z, dim) as f32);
1648                }
1649            }
1650            let mean = hs.iter().sum::<f32>() / hs.len() as f32;
1651            hs.iter().map(|h| (h - mean).powi(2)).sum::<f32>() / hs.len() as f32
1652        };
1653        let d = var(&desierto().0);
1654        let g = var(&pradera().0);
1655        assert!(d < g, "el desierto es más llano: var {d:.1} vs {g:.1}");
1656    }
1657
1658    #[test]
1659    fn el_desierto_pinta_arena_agua_y_cactus() {
1660        let dim = [128, 48, 128];
1661        let (b, pal) = desierto();
1662        let grid = b.generate_window(3, &pal, dim, [0, 0]);
1663        let mut seen = [false; 3]; // [arena, agua, cactus]
1664        let near = |a: [u8; 3], b: [u8; 3]| {
1665            (a[0] as i32 - b[0] as i32).abs() <= 60
1666                && (a[1] as i32 - b[1] as i32).abs() <= 60
1667                && (a[2] as i32 - b[2] as i32).abs() <= 60
1668        };
1669        for z in 0..dim[2] {
1670            for x in 0..dim[0] {
1671                for y in 0..dim[1] {
1672                    if let Some(c) = grid.get(x, y, z) {
1673                        if c[3] == 0 {
1674                            continue;
1675                        }
1676                        let rgb = [c[0], c[1], c[2]];
1677                        if rgb == Material::Water.color() {
1678                            seen[1] = true;
1679                        } else if rgb == Material::Cactus.color() {
1680                            seen[2] = true;
1681                        } else if near(rgb, Material::Sand.color()) {
1682                            seen[0] = true;
1683                        }
1684                    }
1685                }
1686            }
1687        }
1688        assert!(seen[0], "hay arena");
1689        assert!(seen[1], "hay agua (ríos)");
1690        assert!(seen[2], "hay cactus");
1691    }
1692
1693    #[test]
1694    fn grano_de_materia_perturba_pero_es_determinista() {
1695        let base = Material::Sand.color();
1696        assert_eq!(grained(base, 0.0, 5, 3, 9, 1337), base);
1697        let g1 = grained(base, 0.6, 5, 3, 9, 1337);
1698        assert_ne!(g1, base);
1699        let g2 = grained(base, 0.6, 5, 3, 9, 1337);
1700        assert_eq!(g1, g2);
1701        let gn = grained(base, 0.6, 6, 3, 9, 1337);
1702        assert_ne!(g1, gn);
1703        assert!(Material::Sand.grain() > 0.0);
1704    }
1705
1706    #[test]
1707    fn mismo_seed_mismo_mundo() {
1708        let dim = [64, 40, 64];
1709        let (b, pal) = desierto();
1710        let a = b.generate_window(99, &pal, dim, [0, 0]);
1711        let c = b.generate_window(99, &pal, dim, [0, 0]);
1712        for (x, y, z) in [(10, 12, 10), (30, 8, 44), (60, 20, 5)] {
1713            assert_eq!(a.get(x, y, z), c.get(x, y, z), "({x},{y},{z}) difiere");
1714        }
1715    }
1716
1717    /// Cuenta vóxels sólidos en un grid.
1718    fn solids(g: &VoxelGrid, dim: [u32; 3]) -> usize {
1719        let mut n = 0;
1720        for x in 0..dim[0] {
1721            for y in 0..dim[1] {
1722                for z in 0..dim[2] {
1723                    if g.is_solid(x as i32, y as i32, z as i32) {
1724                        n += 1;
1725                    }
1726                }
1727            }
1728        }
1729        n
1730    }
1731
1732    #[test]
1733    fn cada_forma_estampa_voxels() {
1734        let (b, _) = pradera();
1735        let dim = [48, 40, 48];
1736        let leaf = Material::Leaf.color();
1737        // Cada variante debe dejar al menos unos cuantos vóxels sobre el suelo.
1738        for forma in Forma::ALL {
1739            let mut g = VoxelGrid::new(dim);
1740            b.stamp_flora(&mut g, forma, leaf, 24, 4, 24, dim);
1741            let n = solids(&g, dim);
1742            assert!(n >= 3, "{} estampó sólo {n} vóxels", forma.label());
1743        }
1744    }
1745
1746    #[test]
1747    fn formas_lenosas_tienen_tronco() {
1748        let (b, _) = pradera();
1749        let dim = [48, 40, 48];
1750        let leaf = Material::Leaf.color();
1751        // Árbol, conífera y palmera arrancan de un tronco leñoso (WOOD) sobre la base.
1752        for forma in [Forma::Arbol, Forma::Conifera, Forma::Palmera] {
1753            let mut g = VoxelGrid::new(dim);
1754            b.stamp_flora(&mut g, forma, leaf, 24, 4, 24, dim);
1755            let base = g.get(24, 4, 24).map(|c| [c[0], c[1], c[2]]);
1756            assert_eq!(base, Some(WOOD), "{} no tiene tronco en la base", forma.label());
1757        }
1758    }
1759
1760    #[test]
1761    fn el_pueblo_traza_calles_y_plazas() {
1762        let dim = [64, 40, 64];
1763        let (mut b, pal) = pradera();
1764        b.relief = 0.2;
1765        b.mountains = 0.1;
1766        b.water_level = 0.0;
1767        b.rivers = 0.0;
1768        b.casas = vec![crate::casa::CasaUso { tipo: crate::casa::CasaTipo::Cabana, densidad: 0.6 }];
1769        b.densidad_urbana = 0.95; // toda la ventana es pueblo → calles locales garantizadas
1770        let g = b.generate_window(5, &pal, dim, [0, 0]);
1771        // Hay asfalto de calle en la superficie.
1772        let mut asfalto = 0;
1773        for x in 0..dim[0] {
1774            for y in 0..dim[1] {
1775                for z in 0..dim[2] {
1776                    if g.get(x, y, z) == Some([64, 66, 72, 255]) {
1777                        asfalto += 1;
1778                    }
1779                }
1780            }
1781        }
1782        assert!(asfalto > 20, "el pueblo trazó calles (asfalto = {asfalto})");
1783    }
1784
1785    #[test]
1786    fn el_casco_urbano_es_plano_sin_escalones() {
1787        // Bioma ondulado (dunas + relieve): en campo el suelo sube y baja, pero la
1788        // avenida del casco debe salir PLANA — cero escalones entre columnas vecinas.
1789        let dim = [176, 48, 176];
1790        let (mut b, pal) = pradera();
1791        b.base = 0.2;
1792        b.dune = 0.06; // ondulación marcada del terreno
1793        b.relief = 0.3;
1794        b.mountains = 0.2;
1795        b.water_level = 0.0;
1796        b.rivers = 0.0;
1797        b.densidad_urbana = 0.9;
1798        b.casas = vec![crate::casa::CasaUso { tipo: crate::casa::CasaTipo::Cabana, densidad: 0.5 }];
1799        let seed = 9;
1800        let g = b.generate_window(seed, &pal, dim, [0, 0]);
1801
1802        // Ubicamos un pueblo cuyo centro caiga cómodo dentro de la ventana.
1803        let urbe = b.urbe(seed, dim);
1804        let h = |x: i32, z: i32| b.column_height(seed, x, z, dim);
1805        let m = urbe.radio_interior();
1806        let p = crate::urbe::pueblos_en_ventana(&urbe, [0, 0], dim, &h)
1807            .into_iter()
1808            .find(|p| {
1809                p.centro[0] - m >= 0
1810                    && p.centro[1] - m >= 0
1811                    && p.centro[0] + m < dim[0] as i32
1812                    && p.centro[1] + m < dim[2] as i32
1813            })
1814            .expect("hay un pueblo entero dentro de la ventana");
1815
1816        // Recorremos la avenida central (v=0) por el interior y exigimos cota
1817        // constante: el voxel sólido más alto de cada columna de calzada = la
1818        // explanada. Cero escalones entre columnas vecinas.
1819        let base = p.suelo;
1820        let mut vistos = 0;
1821        let mut escalones = 0;
1822        let mut previa: Option<u32> = None;
1823        for du in -urbe.radio_interior()..=urbe.radio_interior() {
1824            let lx = p.centro[0] + du;
1825            let lz = p.centro[1];
1826            if lx < 0 || lz < 0 || lx >= dim[0] as i32 || lz >= dim[2] as i32 {
1827                continue;
1828            }
1829            let y = match g.height_at(lx as u32, lz as u32) {
1830                Some(y) => y,
1831                None => continue,
1832            };
1833            // Sólo contamos columnas de pura calzada (a cota base, sin mueble encima).
1834            if y == base {
1835                vistos += 1;
1836                if let Some(pr) = previa {
1837                    if pr != y {
1838                        escalones += 1;
1839                    }
1840                }
1841                previa = Some(y);
1842            }
1843        }
1844        assert!(vistos > 20, "la avenida nivelada apenas se vio ({vistos} columnas a cota)");
1845        assert_eq!(escalones, 0, "la avenida del casco tiene escalones");
1846
1847        // La misma semilla da el mismo casco (streaming determinista).
1848        let g2 = b.generate_window(seed, &pal, dim, [0, 0]);
1849        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));
1850    }
1851
1852    /// **La siembra es la lista de lo que hay.** Si el bioma estampa un animal y
1853    /// no lo declara, el consumidor SDF lo dibuja al lado de su propio fantasma de
1854    /// voxels; si declara uno que no estampó, aparece un ser flotando.
1855    #[test]
1856    fn la_siembra_declara_lo_que_estampa() {
1857        let dim = [96, 40, 96];
1858        let (b, pal) = pradera_con_bichos();
1859        let (g, siembra) = b.generate_window_con_siembra(7, &pal, dim, [0, 0]);
1860        assert!(!siembra.is_empty(), "el bioma no declaró nada");
1861        assert!(
1862            siembra.iter().any(|s| matches!(s.clase, Sembrada::Fauna { .. })),
1863            "no declaró fauna"
1864        );
1865        // Cada sembrado tiene voxels DE VERDAD en su base (no es una lista de deseos).
1866        for s in &siembra {
1867            let [x, y, z] = s.base;
1868            let alrededor = (0..6)
1869                .flat_map(|dy| {
1870                    (-2i32..=2).flat_map(move |dx| {
1871                        (-2i32..=2).map(move |dz| (dx, dy as u32, dz))
1872                    })
1873                })
1874                .filter(|(dx, dy, dz)| {
1875                    let (px, py, pz) = (x as i32 + dx, (y + dy) as i32, z as i32 + dz);
1876                    px >= 0
1877                        && pz >= 0
1878                        && (px as u32) < dim[0]
1879                        && (py as u32) < dim[1]
1880                        && (pz as u32) < dim[2]
1881                        && g.get(px as u32, py as u32, pz as u32).map(|c| c[3] != 0).unwrap_or(false)
1882                })
1883                .count();
1884            assert!(alrededor > 0, "{} declarado en {:?} y no hay un voxel", s.label(), s.base);
1885        }
1886        // Y es determinista.
1887        let (_, otra) = b.generate_window_con_siembra(7, &pal, dim, [0, 0]);
1888        assert_eq!(siembra, otra);
1889    }
1890
1891    /// **Sacar y volver a poner deja el mundo igual.** Es lo que sostiene el anillo
1892    /// SDF: el que se acerca cambia de técnica y el que se aleja vuelve a la
1893    /// estampa, cuadro tras cuadro, sin que el mundo se erosione.
1894    #[test]
1895    fn desestampar_y_estampar_deja_el_mundo_igual() {
1896        let dim = [96, 40, 96];
1897        let (b, pal) = pradera_con_bichos();
1898        let (mut g, siembra) = b.generate_window_con_siembra(11, &pal, dim, [0, 0]);
1899        let antes = huella_total(&g, dim);
1900        let cuantos = siembra.len().min(12);
1901        for s in siembra.iter().take(cuantos) {
1902            b.desestampar(&mut g, s, dim);
1903        }
1904        let vacio = huella_total(&g, dim);
1905        assert!(vacio < antes, "desestampar no sacó nada ({vacio} vs {antes})");
1906        for s in siembra.iter().take(cuantos) {
1907            b.estampar(&mut g, s, dim);
1908        }
1909        assert_eq!(huella_total(&g, dim), antes, "el mundo no volvió a su lugar");
1910    }
1911
1912    /// Cada sembrado sabe volverse ser analítico, en su sitio y a su escala.
1913    #[test]
1914    fn cada_sembrado_sabe_hacerse_ser_sdf() {
1915        let dim = [96, 40, 96];
1916        let (b, pal) = pradera_con_bichos();
1917        let (_, siembra) = b.generate_window_con_siembra(3, &pal, dim, [0, 0]);
1918        for s in siembra.iter().take(30) {
1919            let ser = s.ser_sdf(1.0, crate::DetalleSer::Hero);
1920            assert!(!ser.prims.is_empty(), "{} no dibujó nada", s.label());
1921            // Posado donde está su estampa (medio voxel de tolerancia por el centrado).
1922            assert!((ser.pos[0] - s.base[0] as f32).abs() <= 1.0);
1923            assert!((ser.pos[2] - s.base[2] as f32).abs() <= 1.0);
1924            assert_eq!(ser.pos[1], s.base[1] as f32);
1925            // Y con la altura de la especie (el voxel es el metro en este motor).
1926            let (lo, hi) = crate::sdf_kit::extension_y(&ser.prims);
1927            assert!(
1928                (hi - lo - s.alto()).abs() < s.alto() * 0.02,
1929                "{} mide {} y su especie {}",
1930                s.label(),
1931                hi - lo,
1932                s.alto()
1933            );
1934        }
1935    }
1936
1937    fn huella_total(g: &VoxelGrid, dim: [u32; 3]) -> usize {
1938        let mut n = 0;
1939        for z in 0..dim[2] {
1940            for y in 0..dim[1] {
1941                for x in 0..dim[0] {
1942                    if g.get(x, y, z).map(|c| c[3] != 0).unwrap_or(false) {
1943                        n += 1;
1944                    }
1945                }
1946            }
1947        }
1948        n
1949    }
1950
1951    fn pradera_con_bichos() -> (Bioma, BiomaPalette) {
1952        let proyecto = crate::Project::starter();
1953        let mut b = proyecto
1954            .biomas
1955            .iter()
1956            .find(|b| b.name == "pradera")
1957            .cloned()
1958            .expect("bioma pradera");
1959        b.fauna = vec![
1960            crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Vaca, densidad: 0.5 },
1961            crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Ciervo, densidad: 0.4 },
1962        ];
1963        let pal = proyecto.bioma_palette(&b);
1964        (b, pal)
1965    }
1966
1967    #[test]
1968    fn la_fauna_puebla_el_bioma() {
1969        let dim = [64, 40, 64];
1970        let (mut b, pal) = pradera();
1971        b.relief = 0.2;
1972        b.mountains = 0.1;
1973        b.water_level = 0.0;
1974        b.rivers = 0.0;
1975        // Sin fauna: el terreno base. Con fauna densa: aparecen animales encima.
1976        let sin = b.generate_window(7, &pal, dim, [0, 0]);
1977        b.fauna = vec![crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Vaca, densidad: 0.95 }];
1978        let con = b.generate_window(7, &pal, dim, [0, 0]);
1979        let mut diff = 0;
1980        for x in 0..dim[0] {
1981            for y in 0..dim[1] {
1982                for z in 0..dim[2] {
1983                    if sin.get(x, y, z) != con.get(x, y, z) {
1984                        diff += 1;
1985                    }
1986                }
1987            }
1988        }
1989        assert!(diff > 20, "la fauna pobló sólo {diff} vóxels");
1990        // Determinista: misma semilla, misma fauna.
1991        let con2 = b.generate_window(7, &pal, dim, [0, 0]);
1992        assert_eq!(con.get(30, 6, 30), con2.get(30, 6, 30));
1993    }
1994
1995    #[test]
1996    fn los_minerales_afloran_en_la_roca() {
1997        let dim = [64, 48, 64];
1998        let (mut b, mut pal) = pradera();
1999        // Roca sólida hasta arriba, mucho relieve → hay profundidad para minar.
2000        pal.ground = ResolvedMaterial::new(Material::Rock.color(), 0.0);
2001        b.relief = 0.9;
2002        b.mountains = 1.0;
2003        b.rivers = 0.0;
2004        b.water_level = 0.0;
2005        let sin = b.generate_window(5, &pal, dim, [0, 0]);
2006        b.minerales = vec![crate::mineral::MineralUso::with_abundancia(crate::mineral::Mineral::Cobre, 1.0)];
2007        let con = b.generate_window(5, &pal, dim, [0, 0]);
2008        let mut diff = 0;
2009        for x in 0..dim[0] {
2010            for y in 0..dim[1] {
2011                for z in 0..dim[2] {
2012                    if sin.get(x, y, z) != con.get(x, y, z) {
2013                        diff += 1;
2014                    }
2015                }
2016            }
2017        }
2018        assert!(diff > 50, "los minerales cambiaron sólo {diff} vóxels");
2019        // Determinista: misma semilla, misma veta.
2020        let con2 = b.generate_window(5, &pal, dim, [0, 0]);
2021        assert_eq!(con.get(20, 10, 20), con2.get(20, 10, 20));
2022    }
2023
2024    /// La gramática sub-voxel llega a TODO lo estampado: flora (detalle del material),
2025    /// fauna (detalle de ser vivo) y minerales (heredan la roca) — no sólo la
2026    /// superficie del terreno. Antes de este cambio, objetos/seres/vetas quedaban
2027    /// SIEMPRE en el look global (`detail_at == [0,0]`), delatando la caja rectangular.
2028    #[test]
2029    fn el_detalle_por_voxel_llega_a_flora_fauna_y_minerales() {
2030        let dim = [64, 44, 64];
2031        let (mut b, mut pal) = pradera();
2032        b.relief = 0.2;
2033        b.mountains = 0.1;
2034        b.water_level = 0.0;
2035        b.rivers = 0.0;
2036        // Flora con detalle autorado (cactus rugoso+biselado) y fauna densa.
2037        pal.objetos = vec![(
2038            ResolvedMaterial::new(Material::Cactus.color(), 0.0).con_detalle((1.1, 0.6)),
2039            0.9,
2040            Forma::Columnar,
2041        )];
2042        b.objetos = vec![ObjetoUso { material: 9, densidad: 0.9, forma: Forma::Columnar }];
2043        b.fauna = vec![crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Vaca, densidad: 0.95 }];
2044        let g = b.generate_window(7, &pal, dim, [0, 0]);
2045
2046        // Cuenta voxels sólidos con detalle autorado (byte != [0,0]) sobre la piel del
2047        // terreno (por encima de la superficie sólo hay flora/fauna). El terreno base
2048        // de este bioma no tiene detalle → todo lo autorado de arriba es flora/fauna.
2049        let mut autorados = 0;
2050        for x in 0..dim[0] {
2051            for z in 0..dim[2] {
2052                for y in 0..dim[1] {
2053                    let solido = g.get(x, y, z).map(|c| c[3] > 127).unwrap_or(false);
2054                    let con_detalle = g.detail_at(x, y, z) != Some([0, 0]);
2055                    if solido && con_detalle {
2056                        autorados += 1;
2057                    }
2058                }
2059            }
2060        }
2061        assert!(autorados > 30, "flora+fauna dejaron sólo {autorados} voxels con detalle");
2062
2063        // Minerales: heredan el detalle de la roca en que están embebidos. Aislo las
2064        // vetas por DIFF (sin/con mineral, misma semilla): todo voxel que aparece como
2065        // mineral debe llevar el detalle de la roca (`cliff` con rug 0.8) → la veta no
2066        // salta como parche de otra fidelidad. Sin color-matching (el mineral se tiñe).
2067        let mut rocoso = pradera().0;
2068        rocoso.relief = 0.9;
2069        rocoso.mountains = 1.0;
2070        rocoso.rivers = 0.0;
2071        rocoso.water_level = 0.0;
2072        let mut pal_roca = pradera().1;
2073        pal_roca.ground = ResolvedMaterial::new(Material::Rock.color(), 0.0);
2074        pal_roca.cliff = ResolvedMaterial::new(Material::Rock.color(), 0.0).con_detalle((0.8, 0.0));
2075        let sin_min = rocoso.generate_window(5, &pal_roca, dim, [0, 0]);
2076        rocoso.minerales =
2077            vec![crate::mineral::MineralUso::with_abundancia(crate::mineral::Mineral::Cobre, 1.0)];
2078        let con_min = rocoso.generate_window(5, &pal_roca, dim, [0, 0]);
2079        let (mut vetas, mut vetas_con_detalle) = (0, 0);
2080        for x in 0..dim[0] {
2081            for y in 0..dim[1] {
2082                for z in 0..dim[2] {
2083                    // Voxel que CAMBIÓ de color por la veta y sigue sólido = mineral.
2084                    let solido = con_min.get(x, y, z).map(|c| c[3] > 127).unwrap_or(false);
2085                    if con_min.get(x, y, z) != sin_min.get(x, y, z) && solido {
2086                        vetas += 1;
2087                        if con_min.detail_at(x, y, z) != Some([0, 0]) {
2088                            vetas_con_detalle += 1;
2089                        }
2090                    }
2091                }
2092            }
2093        }
2094        assert!(vetas > 20, "casi no hubo veta que testear ({vetas} voxels)");
2095        assert_eq!(vetas, vetas_con_detalle, "toda veta hereda el detalle de la roca");
2096    }
2097
2098    #[test]
2099    fn estampado_es_determinista_por_posicion() {
2100        let (b, _) = pradera();
2101        let dim = [48, 40, 48];
2102        let col = Material::Leaf.color();
2103        let mut g1 = VoxelGrid::new(dim);
2104        let mut g2 = VoxelGrid::new(dim);
2105        b.stamp_flora(&mut g1, Forma::Arbol, col, 24, 4, 24, dim);
2106        b.stamp_flora(&mut g2, Forma::Arbol, col, 24, 4, 24, dim);
2107        assert_eq!(solids(&g1, dim), solids(&g2, dim));
2108        // Distinta posición → distinta silueta (el hash siembra con wx/wz).
2109        let mut g3 = VoxelGrid::new(dim);
2110        b.stamp_flora(&mut g3, Forma::Arbol, col, 12, 4, 30, dim);
2111        // La copa cae en otra región; comparamos el conteo local del centro original.
2112        assert!(solids(&g3, dim) > 0);
2113    }
2114}