tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
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}