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