tawasuyu

tawasuyu/llimphi

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

Rust 238TOML 102Markdown 83Shell 24178 Definitions

⎇ main llimphi-voxel/src/studio.rs Rust · 3001 lines · 130.9 KB History Who touched what raw

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1//! `studio` — el **modelo-documento del creador de mundos**: un [`Project`]
2//! agnóstico de UI que junta los *artefactos* (mundos, personajes, escenas) bajo
3//! nombre, para que **una interfaz** los cree/edite y la **IA** los emita o lea.
4//!
5//! Es contenido puro y **serializable** (RON para edición a mano / salida de la IA;
6//! postcard para la CAS): no toca GPU ni ventana. La studio app (o cualquier otra)
7//! lo carga, lo pinta con sus widgets y lo guarda. Cada artefacto referencia tipos
8//! que ya existen en este crate ([`WorldRecipe`], [`Age`]) — el `Project` sólo les
9//! pone nombre y los agrupa.
10
11use serde::{Deserialize, Serialize};
12
13use crate::actor::{Actor, Age, Clip};
14use crate::humano::{humanoide_voxel, Colores, EstiloSer};
15use llimphi_3d::VoxelGrid;
16use crate::director::{ActorKey, ActorScript};
17use crate::bicho::Bicho;
18use crate::conducta::Conducta;
19use crate::ser_sdf::Atuendo;
20use crate::ecuacion::{Assign, Expr, FieldDef, Program, Symbols};
21use crate::casa::{CasaTipo, CasaUso};
22use crate::mineral::{Mineral, MineralUso};
23use crate::rig::{Andar, Movimiento, Rig};
24use crate::worldgen::{Bioma, BiomaPalette, Forma, Material, ResolvedMaterial};
25use llimphi_3d::glam::{Mat4, Vec3};
26use llimphi_3d::{Camera3d, Vertex3d};
27
28/// Dimensión por defecto de la grilla con la que el editor previsualiza un mundo
29/// (cúbica en XZ, alto = 0.4·lado, mínimo 48) — el mismo criterio que la app.
30pub const PREVIEW_DIM_XZ: u32 = 128;
31
32/// **Dirección del sol** de la escena (hacia el sol, sin normalizar). Una sola
33/// fuente de verdad: la usa el preview/export para iluminar (`voxel.sun_dir`) y el
34/// plano [`ShotKind::Backlight`] para saber adónde mirar. Sol algo bajo → luz
35/// rasante (claroscuro) y, en contraluz, **god rays** legibles.
36pub const SCENE_SUN: [f32; 3] = [0.5, 0.36, 0.45];
37
38/// Calcula el `dim` `[x, y, z]` de un mundo de lado `xz` (alto derivado).
39pub fn world_dim(xz: u32) -> [u32; 3] {
40    let dy = (xz * 4 / 10).max(48);
41    [xz, dy, xz]
42}
43
44/// **Paso de reubicación** de la ventana de una escena (voxels). El origen sólo
45/// salta a múltiplos de este valor: así la ventana no se regenera cada cuadro al
46/// caminar el reparto, sólo al cruzar un paso. Como la cámara mira al centroide del
47/// reparto (en espacio de ventana), el salto traslada terreno + cámara + actores
48/// **juntos** → sin pop visible (la imagen es continua), pero el mundo no tiene
49/// borde: el reparto puede caminar indefinidamente. Ver [`window_origin_for_cast`].
50pub const SCENE_WINDOW_STEP: i32 = 16;
51
52/// Redondea `v` al múltiplo de `step` hacia abajo (floor con signo) — espeja el
53/// `snap` del [`WorldStream`](crate::WorldStream).
54#[inline]
55fn snap(v: i32, step: i32) -> i32 {
56    v.div_euclid(step) * step
57}
58
59/// **Origen de ventana (esquina, coords de mundo) que centra al reparto** de una
60/// escena en el instante `t`: promedia las columnas `(gx, gz)` de los guiones
61/// (tomadas en su tiempo cuantizado, igual que al posar) y resta medio `dim`,
62/// snappeado a [`SCENE_WINDOW_STEP`]. Es lo que hace que **toda escena viva en un
63/// mundo infinito**: la ventana voxel sigue al reparto en vez de ser una caja fija
64/// centrada en el origen. Sin reparto, devuelve `[0, 0]` (mundo centrado, como
65/// antes).
66pub fn window_origin_for_cast(scripts: &[ActorScript], t: f32, dim: [u32; 3]) -> [i32; 2] {
67    if scripts.is_empty() {
68        return [0, 0];
69    }
70    let (mut cwx, mut cwz) = (0.0_f32, 0.0_f32);
71    for s in scripts {
72        let smp = s.sample(s.quantize(t));
73        cwx += smp.gx;
74        cwz += smp.gz;
75    }
76    let n = scripts.len() as f32;
77    let (cwx, cwz) = (cwx / n, cwz / n);
78    [
79        snap(cwx as i32 - dim[0] as i32 / 2, SCENE_WINDOW_STEP),
80        snap(cwz as i32 - dim[2] as i32 / 2, SCENE_WINDOW_STEP),
81    ]
82}
83
84// =============================================================================
85//  Nivel 1 — Leyes (físicas / comportamientos)
86// =============================================================================
87
88/// Un **parámetro autorado** de una ley [`Ecuacion`](LeyKind::Ecuacion): nombre (que
89/// la fórmula referencia), valor y rango (para el slider). Es el equivalente dinámico
90/// de los `(nombre, valor, min, max)` cableados de `Fluir`/`Crecer`.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct ParamDef {
93    pub name: String,
94    pub value: f32,
95    pub min: f32,
96    pub max: f32,
97}
98
99impl ParamDef {
100    pub fn new(name: impl Into<String>, value: f32, min: f32, max: f32) -> Self {
101        Self { name: name.into(), value, min, max }
102    }
103}
104
105/// **Tipo de ley** (comportamiento matemático). Enum extensible: `Fluir` (líquidos,
106/// autómata nativo que mueve celdas) y `Crecer` (flora) son leyes **nativas** con
107/// runtime cableado; `Ecuacion` es la ley **autorable** — un sistema de ecuaciones de
108/// campo escrito por el usuario y compilado (ver [`crate::ecuacion`]). Cada variante
109/// nativa expone sus parámetros editables vía [`params`](Self::params) para que la UI
110/// arme sliders; la `Ecuacion` declara sus propios campos y parámetros.
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub enum LeyKind {
113    /// Líquido: se esparce a vecinos en horizontal y cae con gravedad (cascadas).
114    Fluir { gravedad: f32, horizontal: f32 },
115    /// Crece a una velocidad VARIABLE (flora).
116    Crecer { velocidad: f32 },
117    /// **Ley autorable**: campos de estado + parámetros + una ecuación
118    /// `Δcampoₖ/dt = fuenteₖ` por campo (misma cantidad y orden que `campos`). El
119    /// texto `fuentes[k]` se parsea con los nombres de `campos`/`params`. Generaliza
120    /// difusión, reacción‑difusión, calor, crecimiento… con la misma máquina.
121    Ecuacion {
122        campos: Vec<FieldDef>,
123        params: Vec<ParamDef>,
124        fuentes: Vec<String>,
125    },
126}
127
128impl LeyKind {
129    /// Un ejemplar de cada tipo, con valores por defecto (para crear/ciclar).
130    pub fn all_defaults() -> Vec<LeyKind> {
131        vec![
132            LeyKind::Fluir { gravedad: 1.0, horizontal: 0.6 },
133            LeyKind::Crecer { velocidad: 1.0 },
134            LeyKind::ecuacion_default(),
135        ]
136    }
137
138    /// Ley autorable por defecto: **difusión** de un campo `c` (`Δc/dt = k·lap(c)`).
139    /// El punto de partida más simple que ya *hace algo* visible en el laboratorio.
140    pub fn ecuacion_default() -> LeyKind {
141        LeyKind::Ecuacion {
142            campos: vec![FieldDef::new("c", 0.0, 0.0, 1.0)],
143            params: vec![ParamDef::new("k", 0.15, 0.0, 0.25)],
144            fuentes: vec!["k * lap(c)".to_string()],
145        }
146    }
147
148    /// Nombre legible (español).
149    pub fn label(&self) -> &'static str {
150        match self {
151            LeyKind::Fluir { .. } => "fluir",
152            LeyKind::Crecer { .. } => "crecer",
153            LeyKind::Ecuacion { .. } => "ecuación",
154        }
155    }
156
157    /// El tipo siguiente (cicla por los defaults), preservando nada (resetea params).
158    pub fn next(&self) -> LeyKind {
159        let all = Self::all_defaults();
160        let i = all.iter().position(|k| k.label() == self.label()).unwrap_or(0);
161        all[(i + 1) % all.len()].clone()
162    }
163
164    /// Parámetros editables: `(nombre, valor, min, max)`. La UI arma un slider por cada.
165    pub fn params(&self) -> Vec<(&'static str, f32, f32, f32)> {
166        match self {
167            LeyKind::Fluir { gravedad, horizontal } => vec![
168                ("gravedad", *gravedad, 0.0, 1.0),
169                ("horizontal", *horizontal, 0.0, 1.0),
170            ],
171            LeyKind::Crecer { velocidad } => vec![("velocidad", *velocidad, 0.0, 5.0)],
172            // La `Ecuacion` tiene parámetros dinámicos (nombres `String`): no caben en
173            // esta firma `&'static str`; la UI los edita vía [`ecuacion_params`].
174            LeyKind::Ecuacion { .. } => vec![],
175        }
176    }
177
178    /// Fija el parámetro `i` (en el orden de [`params`](Self::params)) a `v`.
179    pub fn set_param(&mut self, i: usize, v: f32) {
180        match self {
181            LeyKind::Fluir { gravedad, horizontal } => match i {
182                0 => *gravedad = v,
183                1 => *horizontal = v,
184                _ => {}
185            },
186            LeyKind::Crecer { velocidad } => {
187                if i == 0 {
188                    *velocidad = v;
189                }
190            }
191            // Para `Ecuacion` se usa [`set_ecuacion_param`] (params dinámicos).
192            LeyKind::Ecuacion { .. } => {}
193        }
194    }
195
196    // --- Autoría de leyes por ecuación -------------------------------------
197
198    /// Parámetros de una ley `Ecuacion` como `(nombre, valor, min, max)` para armar
199    /// sliders. Vacío si no es una `Ecuacion`.
200    pub fn ecuacion_params(&self) -> Vec<(String, f32, f32, f32)> {
201        match self {
202            LeyKind::Ecuacion { params, .. } => {
203                params.iter().map(|p| (p.name.clone(), p.value, p.min, p.max)).collect()
204            }
205            _ => Vec::new(),
206        }
207    }
208
209    /// Valores de los parámetros de la `Ecuacion` en orden (los que consume el motor).
210    pub fn ecuacion_param_values(&self) -> Vec<f32> {
211        match self {
212            LeyKind::Ecuacion { params, .. } => params.iter().map(|p| p.value).collect(),
213            _ => Vec::new(),
214        }
215    }
216
217    /// Fija el valor del parámetro `i` de una ley `Ecuacion` (clampeado a su rango).
218    pub fn set_ecuacion_param(&mut self, i: usize, v: f32) {
219        if let LeyKind::Ecuacion { params, .. } = self {
220            if let Some(p) = params.get_mut(i) {
221                p.value = v.clamp(p.min, p.max);
222            }
223        }
224    }
225
226    /// Tabla de símbolos (campos + params por nombre) para parsear/imprimir sus
227    /// fórmulas. `None` si no es una `Ecuacion`.
228    pub fn ecuacion_symbols(&self) -> Option<Symbols> {
229        match self {
230            LeyKind::Ecuacion { campos, params, .. } => Some(Symbols {
231                campos: campos.iter().map(|c| c.name.clone()).collect(),
232                params: params.iter().map(|p| p.name.clone()).collect(),
233            }),
234            _ => None,
235        }
236    }
237
238    /// Compila una ley `Ecuacion` a un [`Program`] ejecutable: parsea cada fuente con
239    /// los símbolos de la ley y arma un `Δcampoₖ/dt` por campo. `None` si no es
240    /// `Ecuacion`; `Err(msg)` con el primer error de parseo (legible, ubicando el campo).
241    pub fn compile_ecuacion(&self) -> Option<Result<Program, String>> {
242        let LeyKind::Ecuacion { campos, fuentes, .. } = self else {
243            return None;
244        };
245        let sym = self.ecuacion_symbols()?;
246        let mut asignaciones = Vec::with_capacity(fuentes.len());
247        for (i, src) in fuentes.iter().enumerate() {
248            match Expr::parse(src, &sym) {
249                Ok(expr) => asignaciones.push(Assign { campo: i as u16, expr }),
250                Err(err) => {
251                    let campo = campos.get(i).map(|c| c.name.as_str()).unwrap_or("?");
252                    return Some(Err(format!("campo «{campo}»: {err}")));
253                }
254            }
255        }
256        Some(Ok(Program::compile(&asignaciones)))
257    }
258}
259
260/// Una **Ley**: nombre + tipo (con sus parámetros). El nivel más básico de la
261/// composición; los materiales la adoptan vía [`LeyUso`].
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct Ley {
264    pub id: u64,
265    pub name: String,
266    pub kind: LeyKind,
267}
268
269// =============================================================================
270//  Nivel 2 — Materiales
271// =============================================================================
272
273/// **Rol de un material**: relleno de terreno o un objeto colocable (con su forma).
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
275pub enum MatRole {
276    /// Relleno del terreno (suelo, acantilado, cumbre…).
277    Terreno,
278    /// Objeto colocado sobre el suelo (cactus, árbol, roca suelta…).
279    Objeto(Forma),
280}
281
282impl MatRole {
283    pub fn label(&self) -> &'static str {
284        match self {
285            MatRole::Terreno => "terreno",
286            MatRole::Objeto(_) => "objeto",
287        }
288    }
289}
290
291/// **Uso de una ley por un material**: a qué ley refiere y con qué parámetros
292/// concretos (overridean los defaults de la ley — «espesor del líquido», etc.).
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct LeyUso {
295    pub ley: u64,
296    pub params: Vec<f32>,
297}
298
299/// Un **material autorable**: color/grano (texturas a futuro) + rol + leyes
300/// aplicadas, con **herencia viva por padre**. Los campos visuales son `Option`:
301/// `None` = heredar del padre (o del builtin). [`Project::resolve_material`] aplana
302/// la cadena. Así «cactus amarillo» = hijo de «cactus verde» que sólo redefine el
303/// color.
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct MaterialDef {
306    pub id: u64,
307    pub name: String,
308    /// Material del que hereda lo no redefinido (`None` = raíz).
309    #[serde(default)]
310    pub parent: Option<u64>,
311    pub role: MatRole,
312    /// Color `[r,g,b]` en `[0,1]`; `None` = heredar.
313    #[serde(default)]
314    pub color: Option<[f32; 3]>,
315    /// Grano de materia `[0,1]`; `None` = heredar.
316    #[serde(default)]
317    pub grain: Option<f32>,
318    /// Brillo/reflectividad `[0,1]`; `None` = heredar (del padre o del builtin).
319    #[serde(default)]
320    pub brillo: Option<f32>,
321    /// **Rugosidad sub-voxel** `[0,~1.5]` (bump procedural): rompe la cara plana del
322    /// voxel a corta distancia. `None` = heredar (default efectivo 0 = liso). Es un
323    /// look GLOBAL del ray-march; el preview aplica el del material en foco (ver
324    /// `Project::detalle_material`). Roca alta, metal 0.
325    #[serde(default)]
326    pub rugosidad: Option<f32>,
327    /// **Bisel sub-voxel** `[0,1]` (suavizado por isosuperficie): pendientes
328    /// diagonales y esquinas biseladas. `None` = heredar (default 0 = voxel duro).
329    /// También look global del preview.
330    #[serde(default)]
331    pub bisel: Option<f32>,
332    /// Leyes que adopta, con sus parámetros.
333    #[serde(default)]
334    pub leyes: Vec<LeyUso>,
335    /// Tag de paleta semilla (de fábrica): deja que la IA/preset encuentre «la
336    /// arena» sin clavar un id. `None` en materiales hechos por el usuario.
337    #[serde(default)]
338    pub builtin: Option<Material>,
339}
340
341impl MaterialDef {
342    /// Material de fábrica a partir de una variante semilla (color/grano/rol).
343    pub fn from_builtin(id: u64, m: Material) -> Self {
344        let role = match m {
345            Material::Cactus => MatRole::Objeto(Forma::Columnar),
346            Material::Leaf => MatRole::Objeto(Forma::Arbol),
347            Material::Wheat => MatRole::Objeto(Forma::Trigo),
348            Material::Bloom => MatRole::Objeto(Forma::Lavanda),
349            _ => MatRole::Terreno,
350        };
351        let c = m.color();
352        Self {
353            id,
354            name: m.label().to_string(),
355            parent: None,
356            role,
357            color: Some([c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0]),
358            grain: Some(m.grain()),
359            brillo: None, // hereda del builtin (Material::brillo)
360            rugosidad: None,
361            bisel: None,
362            leyes: Vec::new(),
363            builtin: Some(m),
364        }
365    }
366}
367
368// =============================================================================
369//  Nivel 5 — Mundos
370// =============================================================================
371
372/// Un **Mundo**: semilla + uno o más biomas. La semilla es lo único «numérico» que
373/// el usuario teclea (con botón de random en la UI). El stage 1 genera con el primer
374/// bioma; la distribución multi-bioma queda como refinamiento.
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct Mundo {
377    pub id: u64,
378    pub name: String,
379    pub seed: u32,
380    #[serde(default)]
381    pub biomas: Vec<u64>,
382    /// **Fuente de mapa real** (si el mundo se importó de un GeoJSON): persiste la
383    /// receta, no el grid. `None` = mundo procedural (bioma+semilla). Ver
384    /// [`MapaFuente`].
385    #[serde(default)]
386    pub mapa: Option<MapaFuente>,
387}
388
389/// **Fuente de un mundo importado de un mapa real**, persistible dentro del
390/// [`Project`]. Guarda el GeoJSON del visor (y un DEM `.pgm` opcional) **embebidos**
391/// —el documento queda autocontenido— para que el grid se **regenere** al abrir (un
392/// mundo es una receta, no un grid). El puente que lo materializa a `VoxelGrid` vive
393/// en el frontend (`nahual-geo-voxel`), no en este core: aquí sólo son datos.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct MapaFuente {
396    /// GeoJSON crudo (una `FeatureCollection`) del mapa.
397    pub geojson: String,
398    /// DEM opcional (PGM binario P5) embebido; `None` = sin relieve por esta vía.
399    #[serde(default)]
400    pub pgm: Option<Vec<u8>>,
401    /// DEM opcional en **GeoTIFF** (`.tif`) embebido — georreferenciado por sí mismo
402    /// (tiene prioridad sobre `pgm`). `None` = sin relieve por esta vía.
403    #[serde(default)]
404    pub dem_tif: Option<Vec<u8>>,
405    /// Rango de altura `[0, rango]` (metros) al que se mapea el DEM **PGM** (el
406    /// GeoTIFF trae sus metros absolutos, no usa esto).
407    #[serde(default = "default_rango_dem")]
408    pub rango_dem_m: f32,
409}
410
411fn default_rango_dem() -> f32 {
412    300.0
413}
414
415/// Lo que el render necesita de un mundo: el bioma (relieve), la semilla y la paleta
416/// **ya resuelta** (colores concretos). Lo arma [`Project::render_mundo`].
417#[derive(Debug, Clone)]
418pub struct MundoRender {
419    pub bioma: Bioma,
420    pub seed: u32,
421    pub palette: BiomaPalette,
422}
423
424/// **Especificación serializable de un personaje**: lo que un editor/IA fija (edad
425/// + colores). Se materializa con [`to_actor`](Self::to_actor) en un [`Actor`]
426/// posable. Los colores son `[r, g, b]` en `[0,1]` (como [`Actor`]).
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct CharSpec {
429    /// Id estable (asignado por el [`Project`]); `0` hasta que se agrega.
430    #[serde(default)]
431    pub id: u64,
432    pub name: String,
433    pub age: Age,
434    pub skin: [f32; 3],
435    pub shirt: [f32; 3],
436    pub pants: [f32; 3],
437    /// **Pelo** `[r,g,b]` en `[0,1]`. `None` = derivado de la piel; `Some` = color explícito
438    /// (p.ej. muestreado FIEL de una foto). Serde default para compat con proyectos viejos.
439    #[serde(default)]
440    pub hair: Option<[f32; 3]>,
441    /// **Cuerpo**: `None` = humanoide (muñeco [`Actor`] rico: cara, gestos, cross-fade,
442    /// look-at); `Some(mov)` = una morfología arbitraria ([`Movimiento`]: rig +
443    /// andares por estado) — cuadrúpedo, ave, serpiente… Default `None` (compat).
444    #[serde(default)]
445    pub cuerpo: Option<Movimiento>,
446    /// **Conducta** (capa 3): cómo se mueve por el mundo (deambular/gregario/miedo).
447    /// Aplica a cualquier cuerpo (humanoide o rig).
448    #[serde(default)]
449    pub conducta: Conducta,
450    /// **Vestuario** (peinado, barba, sombrero/corona, anteojos, falda, manto). Sólo
451    /// lo viste el camino SDF del humanoide —el que filma escenas—; la malla de cajas
452    /// y el volumen voxel lo ignoran. Un `.ron` viejo, sin la clave, abre desnudo.
453    /// Se escribe SIEMPRE, aunque esté vacío: el documento también viaja en postcard,
454    /// que es posicional, y un campo salteado al escribir desalinea todo lo que sigue
455    /// al leer (`skip_serializing_if` rompió `round_trip_postcard_nativo`).
456    #[serde(default)]
457    pub atuendo: Atuendo,
458    /// **Especie**: `Some(b)` = el ser ES ese animal del bestiario (el elefante del
459    /// rey, el caballo del príncipe) y se filma con su cuerpo SDF
460    /// ([`bicho_sdf_lod`](crate::bicho_sdf_lod)), el mismo de la fauna, dirigido por
461    /// su guion de keyframes como cualquier actor. Gana sobre [`cuerpo`](Self::cuerpo).
462    /// Va al final y se escribe siempre, por lo mismo que el atuendo (postcard).
463    #[serde(default)]
464    pub bicho: Option<Bicho>,
465}
466
467impl CharSpec {
468    /// Un personaje con la paleta por defecto de [`Actor::new`] a la edad dada.
469    /// `id = 0` hasta que el [`Project`] se lo asigna al agregarlo.
470    pub fn new(name: impl Into<String>, age: Age) -> Self {
471        let a = Actor::new(Vec3::ZERO, 0.0);
472        Self {
473            id: 0,
474            name: name.into(),
475            age,
476            skin: a.skin,
477            shirt: a.shirt,
478            pants: a.pants,
479            hair: None,
480            cuerpo: None,
481            conducta: Conducta::default(),
482            atuendo: Atuendo::default(),
483            bicho: None,
484        }
485    }
486
487    /// `true` si el ser se filma como SDF analítico (humanoide o bicho del
488    /// bestiario); `false` si es un rig genérico, que sólo tiene malla de cajas.
489    pub fn es_sdf(&self) -> bool {
490        self.bicho.is_some() || self.cuerpo.is_none()
491    }
492
493    /// Cicla la especie: persona/rig → primer bicho del bestiario → … → último → sin
494    /// especie. `atras` recorre al revés (son setenta: de a uno no se llega nunca).
495    pub fn cycle_bicho(&mut self, atras: bool) {
496        let todos = Bicho::ALL;
497        let n = todos.len();
498        self.bicho = match self.bicho {
499            None => Some(if atras { todos[n - 1] } else { todos[0] }),
500            Some(b) => {
501                let i = todos.iter().position(|&x| x == b).unwrap_or(0);
502                match (atras, i) {
503                    (false, i) if i + 1 < n => Some(todos[i + 1]),
504                    (true, i) if i > 0 => Some(todos[i - 1]),
505                    _ => None,
506                }
507            }
508        };
509    }
510
511    /// Nombre legible del cuerpo (para la UI).
512    pub fn cuerpo_label(&self) -> &str {
513        match &self.cuerpo {
514            None => "humanoide",
515            Some(m) => m.rig.nombre.as_str(),
516        }
517    }
518
519    /// Cicla el cuerpo: humanoide → cuadrúpedo → ave → serpiente → humanoide. Al
520    /// cambiar de rig, regenera andares por defecto (siempre en sync con la morfología).
521    pub fn cycle_cuerpo(&mut self) {
522        let presets = Rig::presets();
523        self.cuerpo = match &self.cuerpo {
524            None => Some(Movimiento::preset(presets[1].clone())), // humanoide → cuadrúpedo
525            Some(m) => {
526                let i = presets.iter().position(|p| p.nombre == m.rig.nombre).unwrap_or(0);
527                if i + 1 < presets.len() {
528                    Some(Movimiento::preset(presets[i + 1].clone()))
529                } else {
530                    None // serpiente → humanoide
531                }
532            }
533        };
534    }
535
536    /// **Meta de render** del ser en `pos`/`facing` para una animación: la matriz de
537    /// modelo + la malla. Humanoide (`rig = None`) usa el [`Actor`] rico (clip, fase,
538    /// look-at); un rig usa su [`Andar::caminar`] al andar (Walk/Run) o pose neutra si
539    /// no. Centraliza el branch para el preview y las escenas.
540    pub fn to_meta(
541        &self,
542        pos: Vec3,
543        facing: f32,
544        clip: Clip,
545        phase: f32,
546        look: Option<Vec3>,
547    ) -> (Mat4, Vec<Vertex3d>, Vec<u16>) {
548        if let Some(b) = self.bicho {
549            // Un bicho no tiene malla propia: cuando el reparto lo baja a cajas (no
550            // entró en el uniform), lo hace el rig genérico de su familia, con su
551            // pelaje y escalado a su alto. Lejos, una silueta que camina alcanza.
552            let receta = crate::bicho_receta::receta(b);
553            let rigs = Rig::presets();
554            let rig = match receta {
555                crate::bicho_receta::Receta::Ave(_) => rigs[2].clone(),
556                crate::bicho_receta::Receta::Reptante(_) | crate::bicho_receta::Receta::Nadador(_) => {
557                    rigs[3].clone()
558                }
559                _ => rigs[1].clone(),
560            };
561            let mov = Movimiento::preset(rig);
562            let pose = match clip {
563                Clip::Walk => mov.andares.caminar.pose(phase),
564                Clip::Run => mov.andares.correr.pose(phase),
565                _ => mov.andares.quieto.pose(phase),
566            };
567            let p = receta.pinta();
568            let (v, i) = mov.rig.mesh(&pose, p.cuerpo, p.vientre, p.detalle);
569            let alto_rig = v.iter().map(|x| x.pos[1]).fold(0.0f32, f32::max).max(0.1);
570            let s = crate::bicho_receta::alto_tipico(b) / alto_rig;
571            let model = Mat4::from_translation(pos)
572                * Mat4::from_rotation_y(facing)
573                * Mat4::from_scale(Vec3::splat(s));
574            return (model, v, i);
575        }
576        match &self.cuerpo {
577            None => {
578                let mut a = self.to_actor(pos, facing);
579                a.set_clip(clip);
580                a.advance(phase);
581                a.look_at(look);
582                let (v, i) = a.mesh();
583                (a.model(), v, i)
584            }
585            Some(mov) => {
586                // El andar según el clip: locomoción (editable) o gesto (procedural).
587                let pose = match clip {
588                    Clip::Walk => mov.andares.caminar.pose(phase),
589                    Clip::Run => mov.andares.correr.pose(phase),
590                    Clip::Wave => Andar::saludar(&mov.rig).pose(phase),
591                    Clip::Point => Andar::senalar(&mov.rig).pose(phase),
592                    Clip::Cheer => Andar::festejar(&mov.rig).pose(phase),
593                    Clip::Idle => mov.andares.quieto.pose(phase),
594                };
595                let (v, i) = mov.rig.mesh(&pose, self.skin, self.shirt, self.pants);
596                let model = Mat4::from_translation(pos) * Mat4::from_rotation_y(facing);
597                (model, v, i)
598            }
599        }
600    }
601
602    /// Materializa el spec en un [`Actor`] parado en `pos` mirando a `facing`.
603    pub fn to_actor(&self, pos: Vec3, facing: f32) -> Actor {
604        Actor::new(pos, facing)
605            .with_age(self.age)
606            .with_colors(self.skin, self.shirt, self.pants)
607    }
608
609    /// **Estilo de personaje** (proporciones/rasgos anclados en literatura) derivado de la
610    /// edad. Ver [`EstiloSer`]. Futuro: guardarlo en el spec para elegir arquetipo a mano.
611    pub fn estilo(&self) -> EstiloSer {
612        EstiloSer::por_edad(self.age)
613    }
614
615    /// **Materializa el ser como VOLUMEN VOXEL redondeado** (no la malla de cajas del
616    /// [`Actor`]): el humanoide estilizado del kit ([`humanoide_voxel`]) en la pose del `clip`/
617    /// `phase`, coloreado por sus rasgos (piel/remera/pantalón), con el detalle sub-voxel
618    /// horneado. `altura` = alto en voxels. Se rinde con `bisel` + `silueta_por_voxel` +
619    /// AO baja para que el ray-marcher lo redondee a un look vinyl-toy/chibi (sin cajas, sin
620    /// valle). Sólo para el humanoide (`cuerpo = None`); un rig usa su propia malla.
621    pub fn to_voxel(&self, clip: Clip, phase: f32, altura: u32) -> VoxelGrid {
622        let pose = clip.pose(phase);
623        let col = match self.hair {
624            Some(h) => Colores::con_pelo(self.skin, self.shirt, self.pants, h),
625            None => Colores::nuevos(self.skin, self.shirt, self.pants),
626        };
627        humanoide_voxel(&self.estilo(), &pose, altura, col)
628    }
629
630    /// **Materializa el ser como SDF ANALÍTICO** ([`ser_sdf`](crate::ser_sdf)): el
631    /// mismo diseño (EstiloSer/Pose/colores) que [`to_voxel`](Self::to_voxel) pero
632    /// sin voxelizar — silueta lisa, cara por decals, sombreado toon en el motor.
633    /// A **escala de Actor** (misma altura que la malla de cajas, ~1.8 para un
634    /// adulto) para que reemplace 1:1 al actor en escenas. Sin posar (local, pies
635    /// en el origen): el caller lo posa con [`SdfSer::posado`](llimphi_3d::SdfSer::posado)
636    /// (coords de voxel `[0, dim]` + yaw). Sólo humanoides (`cuerpo = None`); un
637    /// rig usa su malla propia.
638    pub fn to_sdf(&self, clip: Clip, phase: f32) -> llimphi_3d::SdfSer {
639        self.to_sdf_con(clip, phase, &crate::Gesto::neutro())
640    }
641
642    /// Igual que [`to_sdf`](Self::to_sdf) pero con la **cara del cuadro**
643    /// ([`Gesto`](crate::Gesto)): párpados, boca, pupilas y giro de cabeza.
644    pub fn to_sdf_con(&self, clip: Clip, phase: f32, gesto: &crate::Gesto) -> llimphi_3d::SdfSer {
645        if let Some(b) = self.bicho {
646            return bicho_en_clip(b, clip, phase, crate::DetalleSer::Hero);
647        }
648        let pose = clip.pose(phase);
649        let col = match self.hair {
650            Some(h) => Colores::con_pelo(self.skin, self.shirt, self.pants, h),
651            None => Colores::nuevos(self.skin, self.shirt, self.pants),
652        };
653        let altura = self.to_actor(Vec3::ZERO, 0.0).build.height;
654        crate::ser_sdf::ser_sdf_atuendo(
655            &self.estilo(),
656            &pose,
657            altura,
658            col,
659            gesto,
660            crate::DetalleSer::Hero,
661            &self.atuendo,
662        )
663    }
664
665    /// **El ser vivo, posado y listo para el renderer**: la cara se anima sola con la
666    /// fase ([`Gesto::vivo`](crate::Gesto::vivo) — parpadeo, boca según el clip,
667    /// micro-sacadas) y, si le pasás un `look` (un punto de mundo, típicamente el ojo
668    /// de la cámara), cabeza y pupilas lo siguen.
669    ///
670    /// `pos`/`look` van en las MISMAS coordenadas que consume
671    /// [`VoxelRenderer::sers`](llimphi_3d::VoxelRenderer) (voxel centrado). Es el
672    /// camino que deberían usar preview y export: un solo lugar que decide cómo se ve
673    /// la cara → los dos coinciden cuadro a cuadro.
674    pub fn to_sdf_posado(
675        &self,
676        clip: Clip,
677        phase: f32,
678        pos: Vec3,
679        yaw: f32,
680        look: Option<Vec3>,
681    ) -> llimphi_3d::SdfSer {
682        self.to_sdf_lod(clip, phase, pos, yaw, look, crate::DetalleSer::Hero)
683    }
684
685    /// [`to_sdf_posado`](Self::to_sdf_posado) con el nivel de detalle de la cara a
686    /// elección — lo usa [`reparto_sdf`] para repartir el presupuesto del uniform.
687    pub fn to_sdf_lod(
688        &self,
689        clip: Clip,
690        phase: f32,
691        pos: Vec3,
692        yaw: f32,
693        look: Option<Vec3>,
694        detalle: crate::DetalleSer,
695    ) -> llimphi_3d::SdfSer {
696        if let Some(b) = self.bicho {
697            // El animal no sigue al ojo con la cabeza: eso es de la gente.
698            return bicho_en_clip(b, clip, phase, detalle).posado([pos.x, pos.y, pos.z], yaw);
699        }
700        let mut gesto = crate::Gesto::vivo(phase, clip);
701        let altura = self.to_actor(Vec3::ZERO, 0.0).build.height;
702        if let Some(t) = look {
703            let ojos = crate::altura_ojos(&self.estilo(), altura);
704            gesto = gesto.mirar(t, pos, yaw, ojos);
705        }
706        let pose = clip.pose(phase);
707        let col = match self.hair {
708            Some(h) => Colores::con_pelo(self.skin, self.shirt, self.pants, h),
709            None => Colores::nuevos(self.skin, self.shirt, self.pants),
710        };
711        crate::ser_sdf::ser_sdf_atuendo(&self.estilo(), &pose, altura, col, &gesto, detalle, &self.atuendo)
712            .posado([pos.x, pos.y, pos.z], yaw)
713    }
714
715    /// Cota de primitivas de este ser al `detalle` dado: la cara peor caso
716    /// ([`DetalleSer::costo`](crate::DetalleSer::costo)) más lo que suma su atuendo.
717    /// Es la que tienen que usar los repartos, o un ser vestido se pasa del uniform.
718    pub fn costo(&self, detalle: crate::DetalleSer) -> usize {
719        if let Some(b) = self.bicho {
720            // Medido armándolo: un bicho no tiene cota escrita, y la cantidad de
721            // primitivas no depende de la fase sino del clip (alas, cabeza baja).
722            return [Clip::Idle, Clip::Walk, Clip::Run, Clip::Wave]
723                .into_iter()
724                .map(|c| bicho_en_clip(b, c, 0.0, detalle).prims.len())
725                .max()
726                .unwrap_or(0);
727        }
728        detalle.costo() + self.atuendo.costo_extra(detalle)
729    }
730
731    /// **Dónde tiene la mano** el ser en este clip y fase, en su espacio LOCAL (pies
732    /// en el origen, mirando `+Z`): ahí se apoya la utilería que lleva
733    /// ([`UtileriaSpec::lleva`](crate::UtileriaSpec)). En el humanoide es la mano
734    /// derecha que se dibuja, así que la estrella sube cuando saluda; un bicho la
735    /// lleva sobre el lomo, y un rig genérico, que no tiene mano medida, al costado.
736    pub fn mano(&self, clip: Clip, phase: f32) -> Vec3 {
737        let alto = self.altura();
738        if self.bicho.is_some() {
739            return Vec3::new(0.0, alto, 0.0);
740        }
741        if self.cuerpo.is_some() {
742            return Vec3::new(0.20 * alto, 0.50 * alto, 0.16 * alto);
743        }
744        crate::ser_sdf::mano_derecha(&self.estilo(), &clip.pose(phase), alto)
745    }
746
747    /// Alto del ser en unidades de mundo (el del [`Actor`] equivalente).
748    pub fn altura(&self) -> f32 {
749        if let Some(b) = self.bicho {
750            return crate::bicho_receta::alto_tipico(b);
751        }
752        self.to_actor(Vec3::ZERO, 0.0).build.height
753    }
754}
755
756/// El bicho del bestiario haciendo lo que el clip del guion le pide. El clip es de
757/// la gente (saludar, señalar, festejar); a un animal le llega como lo más parecido
758/// que sabe hacer: andar, trotar, o levantar la cabeza alerta. El que vuela o nada
759/// lo hace siempre — un pez quieto en el aire no es un pez. La `phase` del guion va
760/// en segundos y la del bicho en radianes de su paso: ~0,6 pasos por segundo al
761/// andar, el doble al trotar.
762fn bicho_en_clip(b: Bicho, clip: Clip, phase: f32, detalle: crate::DetalleSer) -> llimphi_3d::SdfSer {
763    use crate::bicho_receta::Receta;
764    use crate::Andanza;
765    let receta = crate::bicho_receta::receta(b);
766    let (andanza, ritmo) = match (&receta, clip) {
767        (Receta::Insecto(_), _) | (Receta::Ave(_), Clip::Run) => (Andanza::Volar, 6.0),
768        (Receta::Nadador(_), _) => (Andanza::Nadar, 3.0),
769        (_, Clip::Walk) => (Andanza::Caminar, 3.8),
770        (_, Clip::Run) => (Andanza::Trotar, 7.5),
771        (_, Clip::Wave | Clip::Point | Clip::Cheer) => (Andanza::Alerta, 2.0),
772        (_, Clip::Idle) => (Andanza::Quieto, 2.0),
773    };
774    crate::bicho_sdf::bicho_sdf_lod(b, crate::bicho_receta::alto_tipico(b), andanza, phase * ritmo, detalle)
775}
776
777/// Un humanoide en el cuadro, tal como lo ve el reparto: quién es, dónde está (coords
778/// de voxel `[0, dim]`, las que consume el renderer), hacia dónde mira el cuerpo y qué
779/// está haciendo.
780#[derive(Debug, Clone, Copy)]
781pub struct EnCuadro<'a> {
782    pub spec: &'a CharSpec,
783    pub pos: Vec3,
784    pub yaw: f32,
785    pub clip: Clip,
786    pub fase: f32,
787}
788
789/// **Arma el reparto SDF de un cuadro dentro del presupuesto del uniform.**
790///
791/// El renderer no tiene lista dinámica: entran [`VOXEL_MAX_SERS`] seres y
792/// [`VOXEL_MAX_SER_PRIMS`] primitivas EN TOTAL. Con la cara completa (~31 prims) eso
793/// son 7 humanoides; una bandada de 16 no entra. Pasarle la lista cruda al empacador
794/// no es una opción: los que sobran se caen del cuadro sin aviso.
795///
796/// Acá se decide con criterio: se ordena por distancia al `ojo`, los de cerca se
797/// llevan la cara entera y los del fondo la pierden ([`DetalleSer`]) — que es donde no
798/// se nota— y si aun así no alcanza, se degrada antes de descartar. Todos miran al
799/// `ojo` y parpadean solos ([`Gesto::vivo`](crate::Gesto::vivo)).
800///
801/// Devuelve los seres de cerca a lejos, listos para `VoxelRenderer::sers`.
802pub fn reparto_sdf(actores: &[EnCuadro<'_>], ojo: Vec3, mirar: bool) -> Vec<llimphi_3d::SdfSer> {
803    use llimphi_3d::{VOXEL_MAX_SERS, VOXEL_MAX_SER_PRIMS};
804
805    let mut orden: Vec<(f32, usize)> = actores
806        .iter()
807        .enumerate()
808        .map(|(i, a)| {
809            let alto = a.spec.altura();
810            let centro = a.pos + Vec3::new(0.0, alto * 0.5, 0.0);
811            (ojo.distance(centro), i)
812        })
813        .collect();
814    orden.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
815
816    let mut out = Vec::new();
817    let mut gastado = 0usize;
818    for (dist, i) in orden {
819        if out.len() >= VOXEL_MAX_SERS {
820            break;
821        }
822        let a = &actores[i];
823        let alto = a.spec.altura();
824        let mut detalle = crate::DetalleSer::por_distancia(dist, alto);
825        // Si no entra con su detalle, se degrada antes de quedar afuera: un ser sin
826        // rasgos al fondo es mucho menos notorio que un ser que desaparece.
827        while gastado + a.spec.costo(detalle) > VOXEL_MAX_SER_PRIMS {
828            detalle = match detalle {
829                crate::DetalleSer::Hero => crate::DetalleSer::Medio,
830                crate::DetalleSer::Medio => crate::DetalleSer::Lejos,
831                crate::DetalleSer::Lejos => break,
832            };
833        }
834        if gastado + a.spec.costo(detalle) > VOXEL_MAX_SER_PRIMS {
835            break; // ni el más barato entra: de acá en adelante tampoco
836        }
837        let look = if mirar { Some(ojo) } else { None };
838        let ser = a
839            .spec
840            .to_sdf_lod(a.clip, a.fase, a.pos, a.yaw, look, detalle);
841        gastado += ser.prims.len();
842        out.push(ser);
843    }
844    out
845}
846
847/// El cuadro repartido entre las **dos técnicas de graficación**: los de cerca como
848/// seres SDF (analíticos, caros y con cara), los del fondo como malla de cajas
849/// (baratos e ilimitados). Ver [`reparto_mixto`].
850pub struct Reparto {
851    /// Los de cerca, listos para `VoxelRenderer::sers` (coords de grilla).
852    pub sers: Vec<llimphi_3d::SdfSer>,
853    /// Los del fondo, listos para `Renderer3d` (espacio **centrado**: `pos − half`).
854    pub mallas: Vec<(Mat4, Vec<Vertex3d>, Vec<u16>)>,
855}
856
857/// **Reparto automático entre técnicas**: el mismo orden por cercanía de
858/// [`reparto_sdf`], pero el que no entra en el presupuesto del uniform **no
859/// desaparece**: se dibuja como malla de cajas, que no tiene tope.
860///
861/// Ése era el agujero: `reparto_sdf` corta cuando ni el detalle más barato entra, y
862/// una multitud de treinta perdía a los últimos catorce sin aviso. Cerca importa la
863/// cara (SDF); a veinte metros, una silueta que se mueve bien alcanza — y la malla la
864/// da gratis.
865///
866/// `half` es la media-dimensión del mundo: las mallas se ubican en el espacio centrado
867/// del `Renderer3d` (`pos − half`), mientras el SDF trabaja en coords de grilla.
868pub fn reparto_mixto(
869    actores: &[EnCuadro<'_>],
870    ojo: Vec3,
871    mirar: bool,
872    half: Vec3,
873) -> Reparto {
874    reparto_mixto_con(actores, ojo, mirar, half, &[])
875}
876
877/// [`reparto_mixto`] cuando en el cuadro ya hay otros SDF que comparten el uniform
878/// —la utilería de la escena—: `ya` se cuenta primero, en lugares y en primitivas,
879/// y los actores se reparten lo que queda. `ya` NO va en el resultado: el caller
880/// lo suma él, que es quien lo armó.
881pub fn reparto_mixto_con(
882    actores: &[EnCuadro<'_>],
883    ojo: Vec3,
884    mirar: bool,
885    half: Vec3,
886    ya: &[llimphi_3d::SdfSer],
887) -> Reparto {
888    use llimphi_3d::{VOXEL_MAX_SERS, VOXEL_MAX_SER_PRIMS};
889
890    let mut orden: Vec<(f32, usize)> = actores
891        .iter()
892        .enumerate()
893        .map(|(i, a)| {
894            let alto = a.spec.altura();
895            let centro = a.pos + Vec3::new(0.0, alto * 0.5, 0.0);
896            (ojo.distance(centro), i)
897        })
898        .collect();
899    orden.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
900
901    let mut rep = Reparto { sers: Vec::new(), mallas: Vec::new() };
902    let mut gastado: usize = ya.iter().map(|s| s.prims.len()).sum();
903    let mut de_malla = false; // una vez que uno cae a malla, los de atrás también
904    for (dist, i) in orden {
905        let a = &actores[i];
906        let alto = a.spec.altura();
907        let look = if mirar { Some(ojo) } else { None };
908        if !de_malla && ya.len() + rep.sers.len() < VOXEL_MAX_SERS {
909            let mut detalle = crate::DetalleSer::por_distancia(dist, alto);
910            while gastado + a.spec.costo(detalle) > VOXEL_MAX_SER_PRIMS {
911                detalle = match detalle {
912                    crate::DetalleSer::Hero => crate::DetalleSer::Medio,
913                    crate::DetalleSer::Medio => crate::DetalleSer::Lejos,
914                    crate::DetalleSer::Lejos => break,
915                };
916            }
917            if gastado + a.spec.costo(detalle) <= VOXEL_MAX_SER_PRIMS {
918                let ser = a.spec.to_sdf_lod(a.clip, a.fase, a.pos, a.yaw, look, detalle);
919                gastado += ser.prims.len();
920                rep.sers.push(ser);
921                continue;
922            }
923        }
924        de_malla = true;
925        let look_centrado = look.map(|o| o - half);
926        rep.mallas.push(a.spec.to_meta(a.pos - half, a.yaw, a.clip, a.fase, look_centrado));
927    }
928    rep
929}
930
931/// **Keyframe serializable** de un actor (espejo de [`ActorKey`]): dónde está en
932/// la grilla en `t`, y opcionalmente qué clip reproduce y hacia dónde mira.
933#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
934pub struct ActorKeySpec {
935    pub t: f32,
936    pub gx: f32,
937    pub gz: f32,
938    #[serde(default)]
939    pub clip: Option<Clip>,
940    #[serde(default)]
941    pub face: Option<f32>,
942}
943
944impl ActorKeySpec {
945    /// Compila a un [`ActorKey`] del director.
946    pub fn to_key(self) -> ActorKey {
947        let mut k = ActorKey::at(self.t, self.gx, self.gz);
948        if let Some(c) = self.clip {
949            k = k.play(c);
950        }
951        if let Some(f) = self.face {
952            k = k.facing(f);
953        }
954        k
955    }
956}
957
958/// **Actor de una escena**: qué personaje del proyecto lo interpreta (`character`,
959/// índice en [`Project::characters`]) y su guion de keyframes.
960#[derive(Debug, Clone, Serialize, Deserialize)]
961pub struct ActorSpec {
962    pub character: usize,
963    pub keys: Vec<ActorKeySpec>,
964    /// **Tasa de cuadros propia** del actor (`None` = fluido/nativo). Con un valor
965    /// bajo (12–15) el actor se anima *en doses* (stop-motion): es el sello que
966    /// separa al Héroe del Avatar. Ver [`ActorScript::quantize`].
967    #[serde(default)]
968    pub frame_rate: Option<u32>,
969}
970
971impl ActorSpec {
972    /// Compila los keyframes a un [`ActorScript`] reproducible (con su tasa de
973    /// cuadros propia, si la tiene).
974    pub fn to_script(&self) -> ActorScript {
975        ActorScript::new(self.keys.iter().map(|k| k.to_key()).collect())
976            .with_frame_rate(self.frame_rate)
977    }
978}
979
980/// **Tipo de plano** de cámara: un encuadre cinematográfico de alto nivel que se
981/// resuelve contra el **centroide del reparto** (no contra `eye/target` crudos), así
982/// es trivial de elegir y de generar por IA. [`resolve`](Self::resolve) produce la
983/// [`Camera3d`] del frame.
984#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
985pub enum ShotKind {
986    /// Establecedor: lejos y alto, presenta la escena.
987    Establishing,
988    /// Primer plano: cerca, a la altura del pecho.
989    CloseUp,
990    /// Lateral: desde el costado.
991    Side,
992    /// Órbita: gira lento alrededor del reparto.
993    Orbit,
994    /// **Contraluz**: cámara del lado opuesto al sol, mirando hacia él con el reparto
995    /// a contraluz (silueta en el tercio bajo) y el disco solar en lo alto del cuadro
996    /// → rim light + **god rays** (haces volumétricos). El plano "héroe" del motor.
997    Backlight,
998    /// **Frontal**: de frente y sin sesgo de costado, un poco alto. Es el plano
999    /// de la arquitectura: el establecedor se corre media distancia a un lado y
1000    /// un palacio simétrico, filmado así, se lee torcido. Va al final: postcard
1001    /// numera las variantes.
1002    Frontal,
1003}
1004
1005impl ShotKind {
1006    /// Todos los planos (para ciclar en un editor).
1007    pub const ALL: [ShotKind; 6] = [
1008        ShotKind::Establishing,
1009        ShotKind::CloseUp,
1010        ShotKind::Side,
1011        ShotKind::Orbit,
1012        ShotKind::Backlight,
1013        ShotKind::Frontal,
1014    ];
1015
1016    /// Nombre legible (español).
1017    pub fn label(self) -> &'static str {
1018        match self {
1019            ShotKind::Establishing => "establecedor",
1020            ShotKind::CloseUp => "primer plano",
1021            ShotKind::Side => "lateral",
1022            ShotKind::Orbit => "órbita",
1023            ShotKind::Backlight => "contraluz",
1024            ShotKind::Frontal => "frontal",
1025        }
1026    }
1027
1028    /// El plano siguiente (cicla).
1029    pub fn next(self) -> ShotKind {
1030        let i = ShotKind::ALL.iter().position(|&k| k == self).unwrap_or(0);
1031        ShotKind::ALL[(i + 1) % ShotKind::ALL.len()]
1032    }
1033
1034    /// Resuelve la cámara del plano: mira a `look` (centroide del reparto, ya
1035    /// elevado a la altura del pecho), con el ojo según el tipo, a distancia base
1036    /// `d` (escala con el tamaño del reparto). `t` (seg) anima la órbita.
1037    pub fn resolve(self, look: Vec3, d: f32, t: f32) -> Camera3d {
1038        // `(eye, fov, target)` por tipo. Casi todos miran al reparto (`look`); el
1039        // contraluz sube el objetivo hacia el sol para meterlo en el cuadro.
1040        let (eye, fov, target) = match self {
1041            ShotKind::Establishing => {
1042                (look + Vec3::new(-0.5 * d, 0.9 * d, -1.6 * d), 50.0, look)
1043            }
1044            ShotKind::CloseUp => (look + Vec3::new(0.25 * d, 0.45 * d, -0.85 * d), 40.0, look),
1045            // El objetivo sube un poco: con el reparto en el tercio bajo, lo que
1046            // tiene detrás (el palacio) entra entero en el cuadro.
1047            //
1048            // Y todo el plano se TRASLADA 0,06·d a un costado (sin girar): con el
1049            // ojo justo sobre el plano de simetría de lo filmado, el ray-march
1050            // dibuja una raya oscura de arriba abajo por el eje (medido en
1051            // Margarita: la cúpula, el portal y el camino partidos por la mitad;
1052            // corriendo el ojo 0,3 voxel se atenúa, con 0,9 desaparece). La causa
1053            // en el shader está sin diagnosticar; esto evita el caso degenerado
1054            // que el plano frontal provoca a propósito.
1055            ShotKind::Frontal => {
1056                let lado = Vec3::new(0.06 * d, 0.0, 0.0);
1057                (look + lado + Vec3::new(0.0, 0.55 * d, -1.7 * d), 50.0, look + lado + Vec3::new(0.0, 0.35 * d, 0.0))
1058            }
1059            ShotKind::Side => (look + Vec3::new(1.35 * d, 0.4 * d, 0.15 * d), 46.0, look),
1060            ShotKind::Orbit => {
1061                let a = t * 0.6;
1062                (look + Vec3::new(a.cos() * 1.3 * d, 0.6 * d, a.sin() * 1.3 * d), 48.0, look)
1063            }
1064            ShotKind::Backlight => {
1065                // Detrás del reparto respecto al sol (horizontal anti-sol), apenas
1066                // elevado; el objetivo sube hacia el cielo → la cámara mira hacia el
1067                // sol con el reparto a contraluz en el tercio bajo.
1068                let s = Vec3::new(SCENE_SUN[0], SCENE_SUN[1], SCENE_SUN[2]).normalize();
1069                let back = Vec3::new(-s.x, 0.0, -s.z).normalize();
1070                // El objetivo sube 0,4·d: el reparto cae a ~19° bajo el centro,
1071                // en el tercio bajo de un cuadro de 55°. Con 0,55·d caía a 25°,
1072                // pegado al borde (medido en Margarita: de la princesa asomaba
1073                // la coronilla), y el sol/luna a 28° de alto sigue entrando.
1074                let eye = look + back * (1.2 * d) + Vec3::new(0.0, 0.15 * d, 0.0);
1075                let target = look + Vec3::new(0.0, 0.4 * d, 0.0);
1076                (eye, 55.0, target)
1077            }
1078        };
1079        Camera3d { eye, target, fovy_rad: fov_f32_to_rad(fov), ..Camera3d::default() }
1080    }
1081}
1082
1083/// Grados → radianes (helper local para no depender de glam en el call site).
1084fn fov_f32_to_rad(deg: f32) -> f32 {
1085    deg * std::f32::consts::PI / 180.0
1086}
1087
1088/// Un **plano** de la escena: el tipo de encuadre y desde qué instante (seg) está
1089/// activo. El plano vigente en `t` es el último con `start ≤ t` (corte duro entre
1090/// planos).
1091#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1092pub struct ShotSpec {
1093    pub start: f32,
1094    pub kind: ShotKind,
1095}
1096
1097/// Qué clase de SUCESO se dicta en un punto de la escena. Se dictan desde
1098/// adentro (caminando/volando el mundo) y se autoran aquí.
1099#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1100pub enum TipoSuceso {
1101    /// Aparece un ser (id en [`Project::seres`]; `0` = todavía por asignar).
1102    Aparece(u64),
1103    /// La cámara mira a este punto (una marca para dirigir un plano).
1104    Camara,
1105    /// Punto de interés genérico — "aquí pasa algo".
1106    Marca,
1107    /// **El mundo se desmorona acá**: al cruzar el instante del suceso, `motas`
1108    /// voxels de la esfera `radio` se desprenden y salen a volar como ceniza
1109    /// ([`Enjambre`](crate::Enjambre)). Es el efecto de crisis del guion, dictado
1110    /// como cualquier otro suceso — con su instante y su lugar, no como un botón.
1111    Desmorona { radio: f32, motas: u32 },
1112}
1113
1114/// Un SUCESO dictado en la escena: qué pasa, dónde y cuándo. El creador lo deja
1115/// caer mientras recorre el mundo en primera persona.
1116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1117pub struct Suceso {
1118    /// Instante en la escena (segundos).
1119    pub t: f32,
1120    /// Dónde, en coordenadas del mundo/grilla `[x, y, z]`.
1121    pub pos: [f32; 3],
1122    /// Qué clase de suceso.
1123    pub tipo: TipoSuceso,
1124    /// Nota libre del creador ("aquí despierta", "corte a primer plano"…).
1125    pub nota: String,
1126}
1127
1128/// **Especificación serializable de una escena**: el mundo de fondo (`world`,
1129/// índice en [`Project::worlds`]), la duración, el reparto guionado y los **planos**
1130/// de cámara. Es la versión editable/IA-emisible del [`Sequence`](crate::Sequence)
1131/// del director; se compila con [`scripts`](Self::scripts) y se reproduce posando
1132/// cada actor en `sample(t)` con la cámara del plano vigente.
1133#[derive(Debug, Clone, Serialize, Deserialize)]
1134pub struct SceneSpec {
1135    /// Id estable (asignado por el [`Project`]).
1136    #[serde(default)]
1137    pub id: u64,
1138    pub name: String,
1139    /// El **mundo** de fondo (id en [`Project::mundos`]).
1140    pub mundo: u64,
1141    pub duration: f32,
1142    pub actors: Vec<ActorSpec>,
1143    #[serde(default)]
1144    pub shots: Vec<ShotSpec>,
1145    /// **Cámara en mano**: intensidad del temblor orgánico (`0` = trípode fijo,
1146    /// look de dron; `~1` = respiración/pulso de camarógrafo). Ensucia la cámara
1147    /// matemáticamente perfecta del motor — es el sello que mete al espectador en
1148    /// el "barro" de la escena. Ver [`handheld_shake`].
1149    #[serde(default)]
1150    pub handheld: f32,
1151    /// **Cuán cerca está la cámara**, en unidades del mundo. `None` ⇒ la
1152    /// distancia se deduce del tamaño del reparto (`6 + actores × 1,2`), que es
1153    /// lo que había y lo que sirve para una escena con gente.
1154    ///
1155    /// Existe porque esa deducción supone que lo filmado es una ESCENA, y una
1156    /// escena de un solo ser que se muestra como objeto —un emblema, una pieza
1157    /// de museo— pide lo contrario: acercarse hasta que la cosa llene el
1158    /// cuadro. Con la fórmula, un animal solo salía del tamaño de una moneda
1159    /// en medio de un campo, que es «mostrar el campo», no mostrarlo a él.
1160    #[serde(default)]
1161    pub distancia: Option<f32>,
1162    /// **Atmósfera** de apertura: la bruma + el fundido desde negro. `None` =
1163    /// aire despejado, sin fundido (compatibilidad con escenas viejas).
1164    #[serde(default)]
1165    pub atmosfera: Option<crate::apertura::Atmosfera>,
1166    /// **Caída** de apertura: la cámara pura cae del cielo (sin cuerpo) hasta
1167    /// aterrizar. `None` = cámara normal por planos.
1168    #[serde(default)]
1169    pub caida: Option<crate::apertura::Caida>,
1170    /// **Sucesos** dictados desde adentro: qué pasa, dónde y cuándo.
1171    #[serde(default)]
1172    pub sucesos: Vec<Suceso>,
1173    /// **Utilería**: la estrella, la flor, la perla… puestas en el mundo o en la
1174    /// mano de un actor, con su instante de entrada y de salida. Va al final y se
1175    /// escribe siempre (postcard es posicional).
1176    #[serde(default)]
1177    pub utileria: Vec<crate::UtileriaSpec>,
1178    /// **Cielo de noche** (estrellas, luna, luz de luna). `None` = de día, con
1179    /// la luz de quien filma. Va al final y se escribe siempre (postcard).
1180    #[serde(default)]
1181    pub cielo: Option<crate::Cielo>,
1182    /// **Decorado**: el palacio, el kiosko… plantados a mano en su lugar del
1183    /// mundo. Va al final y se escribe siempre (postcard).
1184    #[serde(default)]
1185    pub decorado: Vec<crate::Decorado>,
1186    /// **Jardín**: explanada llana, caminos de tierra y una orla de flores y
1187    /// plantas. `None` = el relieve tal cual. Va al final y se escribe siempre
1188    /// (postcard).
1189    #[serde(default)]
1190    pub jardin: Option<crate::Jardin>,
1191}
1192
1193/// **Temblor de cámara en mano**, determinista (función pura de `t` → la peli sale
1194/// reproducible cuadro a cuadro). Suma de senos en frecuencias inconmensurables:
1195/// una **respiración** lenta (bob vertical) + un **micro-pulso** rápido en los tres
1196/// ejes para el ojo, y una **deriva** aún más lenta para el objetivo (el encuadre
1197/// flota, no sólo tiembla). `amt ≤ 0` → sin offset (trípode). Devuelve
1198/// `(offset_ojo, offset_objetivo)` en unidades de mundo, escalado un poco con la
1199/// distancia `d` del plano para que también respire en planos lejanos.
1200pub fn handheld_shake(t: f32, amt: f32, d: f32) -> (Vec3, Vec3) {
1201    if amt <= 0.0 {
1202        return (Vec3::ZERO, Vec3::ZERO);
1203    }
1204    let scale = amt * (1.0 + d * 0.03);
1205    // Respiración (bob) + micro-pulso por eje (fases y frecuencias dispares).
1206    let breath = (t * 1.7).sin() * 0.6 + (t * 0.9).sin() * 0.4;
1207    let jx = (t * 9.3).sin() * 0.5 + (t * 4.7 + 1.3).sin() * 0.5;
1208    let jy = (t * 8.1 + 2.1).sin() * 0.5 + (t * 5.3 + 0.7).sin() * 0.5;
1209    let jz = (t * 7.4 + 0.4).sin() * 0.5 + (t * 3.9 + 2.7).sin() * 0.5;
1210    let eye = Vec3::new(jx * 0.10, breath * 0.12 + jy * 0.08, jz * 0.10) * scale;
1211    // Deriva del objetivo: más lenta y desfasada → el cuadro "busca" al sujeto.
1212    let tgt = Vec3::new(
1213        (t * 1.3 + 0.5).sin() * 0.06,
1214        (t * 1.1 + 1.9).sin() * 0.05,
1215        0.0,
1216    ) * scale;
1217    (eye, tgt)
1218}
1219
1220impl SceneSpec {
1221    /// La **utilería** que se ve en `t`, posada y lista para el renderer (coords de
1222    /// grilla). `actores` son los de [`actors`](Self::actors) en ese instante, en su
1223    /// orden: `(pies en coords de grilla, rumbo, mano local)` — la mano sale de
1224    /// [`CharSpec::mano`] con el clip y la fase del cuadro. Se corta a lo que entra en un cuarto del uniform: una
1225    /// escena con cien perlas no puede dejar sin cara al reparto.
1226    pub fn utileria_en(&self, t: f32, actores: &[(Vec3, f32, Vec3)]) -> Vec<llimphi_3d::SdfSer> {
1227        use llimphi_3d::{VOXEL_MAX_SERS, VOXEL_MAX_SER_PRIMS};
1228        let mut out = Vec::new();
1229        let mut gastado = 0usize;
1230        for u in &self.utileria {
1231            let Some(s) = u.en_t(t, actores) else { continue };
1232            if out.len() >= VOXEL_MAX_SERS / 4 || gastado + s.prims.len() > VOXEL_MAX_SER_PRIMS / 4 {
1233                break;
1234            }
1235            gastado += s.prims.len();
1236            out.push(s);
1237        }
1238        out
1239    }
1240
1241    /// Las **luces** que da la utilería con luz propia en `t` (la estrella que
1242    /// alumbra), en coords de grilla, con los mismos `actores` que
1243    /// [`utileria_en`](Self::utileria_en). Sólo **de noche**: de día no se notan y
1244    /// cada una cuesta un rayo de sombra por píxel. El alcance crece con la pieza
1245    /// cuando aparece. Se corta al máximo de luces del renderer.
1246    pub fn luces_en(&self, t: f32, actores: &[(Vec3, f32, Vec3)]) -> Vec<llimphi_3d::PointLight> {
1247        if self.cielo.is_none() {
1248            return Vec::new();
1249        }
1250        let mut out = Vec::new();
1251        for u in self.utileria.iter().filter(|u| u.cosa.luz_propia()) {
1252            if out.len() >= llimphi_3d::VOXEL_MAX_LIGHTS {
1253                break;
1254            }
1255            let Some(s) = u.en_t(t, actores) else { continue };
1256            let (c, _) = s.bound();
1257            let centro = Vec3::from_array(s.pos) + crate::sdf_kit::rot_y(Vec3::from_array(c), s.yaw);
1258            let color = u.color.unwrap_or(u.cosa.color());
1259            out.push(crate::luz_de(centro, color, crate::ALCANCE_LUZ_PROPIA * u.escala_en(t)));
1260        }
1261        out
1262    }
1263
1264    /// Los guiones de los actores, listos para `sample(t)`.
1265    /// **Lo que la escena le pone al mundo** encima del relieve: el jardín, los
1266    /// corredores por donde camina cada actor (las columnas de sus keys, en
1267    /// orden) y el decorado. La ventana lo estampa cada vez que se arma.
1268    pub fn puesta(&self) -> crate::Puesta {
1269        crate::Puesta {
1270            jardin: self.jardin.clone(),
1271            andares: self.actors.iter().map(|a| a.keys.iter().map(|k| [k.gx, k.gz]).collect()).collect(),
1272            decorado: self.decorado.clone(),
1273        }
1274    }
1275
1276    pub fn scripts(&self) -> Vec<ActorScript> {
1277        self.actors.iter().map(|a| a.to_script()).collect()
1278    }
1279
1280    /// Los **instantes (seg) que merecen un acento musical**: los cortes de cámara
1281    /// (inicio de cada plano salvo el primero) y los **gestos** de los actores (keys
1282    /// con un clip *emote*). Es lo que deja caer la banda sonora *sobre la acción*.
1283    /// Ordenados, sin repetir (dos a menos de `EPS` se funden). Espeja
1284    /// [`Sequence::beat_times`](crate::Sequence::beat_times).
1285    pub fn beat_times(&self) -> Vec<f32> {
1286        const EPS: f32 = 0.05;
1287        let mut ts: Vec<f32> = Vec::new();
1288        for s in self.shots.iter().skip(1) {
1289            ts.push(s.start);
1290        }
1291        for a in &self.actors {
1292            for k in &a.keys {
1293                if k.clip.is_some_and(|c| c.is_emote()) {
1294                    ts.push(k.t);
1295                }
1296            }
1297        }
1298        ts.retain(|&t| t >= 0.0 && t <= self.duration + EPS);
1299        ts.sort_by(f32::total_cmp);
1300        ts.dedup_by(|a, b| (*a - *b).abs() < EPS);
1301        ts
1302    }
1303
1304    /// La **cámara de la escena** en `t`: resuelve el plano vigente mirando a
1305    /// `look` (centroide del reparto) a distancia `d`, y le suma el temblor de
1306    /// **cámara en mano** ([`handheld_shake`]) según [`Self::handheld`]. Es el
1307    /// único punto por el que deberían pasar el preview y el export para que el
1308    /// sello de cámara salga igual en ambos.
1309    pub fn camera_at(&self, look: Vec3, d: f32, t: f32) -> Camera3d {
1310        // Apertura por CAÍDA (cámara pura, sin cuerpo): mientras dura la caída, la
1311        // cámara desciende del cielo hacia `look`; al aterrizar, cede a los planos.
1312        if let Some(caida) = self.caida {
1313            if t < caida.duracion {
1314                let mut cam = caida.camara_desde(look, t);
1315                let (eo, to) = handheld_shake(t, self.handheld, d);
1316                cam.eye += eo;
1317                cam.target += to;
1318                return cam;
1319            }
1320        }
1321        let mut cam = self.active_shot(t).resolve(look, d, t);
1322        let (eo, to) = handheld_shake(t, self.handheld, d);
1323        cam.eye += eo;
1324        cam.target += to;
1325        cam
1326    }
1327
1328    /// El plano vigente en `t` (el último con `start ≤ t`); `Establishing` si no
1329    /// hay planos definidos.
1330    pub fn active_shot(&self, t: f32) -> ShotKind {
1331        self.shots
1332            .iter()
1333            .filter(|s| s.start <= t)
1334            .last()
1335            .map(|s| s.kind)
1336            .unwrap_or(ShotKind::Establishing)
1337    }
1338
1339    /// **Escena patrón "entran y saludan"**: `n` actores entran caminando por el
1340    /// centro del mundo de izquierda a derecha, se giran y hacen `gesture`. Coords
1341    /// de grilla (la altura del terreno se aplica al reproducir). La base tanto del
1342    /// arranque como de la generación por IA.
1343    pub fn walk_and_emote(
1344        name: impl Into<String>,
1345        mundo: u64,
1346        n: usize,
1347        gesture: Clip,
1348        dim: [u32; 3],
1349    ) -> Self {
1350        use std::f32::consts::{FRAC_PI_2, PI};
1351        let n = n.clamp(1, 5);
1352        let margin = 18.0_f32;
1353        let gx0 = margin;
1354        let gx1 = (dim[0] as f32 - margin).max(gx0 + 1.0);
1355        let cz = dim[2] as f32 * 0.5;
1356        let (t_walk, t_turn, dur) = (2.6_f32, 3.0_f32, 5.6_f32);
1357
1358        let mut actors = Vec::with_capacity(n);
1359        for i in 0..n {
1360            let off = (i as f32 - (n as f32 - 1.0) / 2.0) * 3.0;
1361            let gz = cz + off;
1362            actors.push(ActorSpec {
1363                character: i,
1364                keys: vec![
1365                    ActorKeySpec { t: 0.0, gx: gx0, gz, clip: None, face: None },
1366                    ActorKeySpec { t: t_walk, gx: gx1, gz, clip: None, face: Some(FRAC_PI_2) },
1367                    ActorKeySpec { t: t_turn, gx: gx1, gz, clip: Some(gesture), face: Some(PI) },
1368                    ActorKeySpec { t: dur, gx: gx1, gz, clip: Some(gesture), face: Some(PI) },
1369                ],
1370                // El **Héroe** (primer actor) se anima en doses (12 fps): se mueve a
1371                // tirones, pesado, contra los demás (Avatares) que van fluidos. Es el
1372                // sello de animación, visible ya en la escena de arranque.
1373                frame_rate: if i == 0 { Some(12) } else { None },
1374            });
1375        }
1376        // Tres planos: establecedor, **contraluz** a mitad de la caminata (el reparto
1377        // entra al sol → luce los god rays), y primer plano en el gesto.
1378        let shots = vec![
1379            ShotSpec { start: 0.0, kind: ShotKind::Establishing },
1380            ShotSpec { start: t_walk * 0.5, kind: ShotKind::Backlight },
1381            ShotSpec { start: t_turn, kind: ShotKind::CloseUp },
1382        ];
1383        // Cámara en mano suave por defecto: el sello se ve sin tener que pedirlo.
1384        Self {
1385            id: 0,
1386            name: name.into(),
1387            mundo,
1388            duration: dur,
1389            actors,
1390            shots,
1391            handheld: 0.7,
1392            // La distancia sale del tamaño del reparto: esto arma una escena
1393            // con gente caminando, que es justo el caso para el que esa
1394            // deducción sirve.
1395            distancia: None,
1396            atmosfera: None,
1397            caida: None,
1398            sucesos: Vec::new(),
1399            utileria: Vec::new(),
1400            cielo: None,
1401            decorado: Vec::new(),
1402            jardin: None,
1403        }
1404    }
1405}
1406
1407/// El **proyecto**: la bolsa de artefactos del creador, por niveles de composición
1408/// (leyes → materiales → seres → biomas → mundos → escenas). Cada item lleva un `id`
1409/// estable (separado del nombre) para referenciarse, renombrarse y duplicarse.
1410/// Vacío por defecto; [`starter`](Self::starter) trae algo que tocar.
1411#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1412pub struct Project {
1413    /// Contador de ids; `0` queda reservado como «ninguno».
1414    #[serde(default)]
1415    pub next_id: u64,
1416    #[serde(default)]
1417    pub leyes: Vec<Ley>,
1418    #[serde(default)]
1419    pub materiales: Vec<MaterialDef>,
1420    /// Los **seres** (personajes); el tipo se llama `CharSpec` por historia.
1421    #[serde(default)]
1422    pub seres: Vec<CharSpec>,
1423    #[serde(default)]
1424    pub biomas: Vec<Bioma>,
1425    #[serde(default)]
1426    pub mundos: Vec<Mundo>,
1427    #[serde(default)]
1428    pub escenas: Vec<SceneSpec>,
1429    /// Interiores amueblables (editor de interiores).
1430    #[serde(default)]
1431    pub interiores: Vec<crate::interior::Interior>,
1432}
1433
1434impl Project {
1435    /// Reserva el próximo id estable.
1436    pub fn alloc_id(&mut self) -> u64 {
1437        if self.next_id == 0 {
1438            self.next_id = 1;
1439        }
1440        let id = self.next_id;
1441        self.next_id += 1;
1442        id
1443    }
1444
1445    /// Proyecto de arranque: leyes base, los 6 materiales semilla, un trío de seres,
1446    /// los biomas desierto/pradera y dos mundos que los envuelven, más la escena demo.
1447    pub fn starter() -> Self {
1448        let mut p = Project { next_id: 1, ..Default::default() };
1449
1450        // --- Leyes base ---
1451        let ley_fluir = p.alloc_id();
1452        p.leyes.push(Ley {
1453            id: ley_fluir,
1454            name: "fluir".into(),
1455            kind: LeyKind::Fluir { gravedad: 1.0, horizontal: 0.6 },
1456        });
1457        let ley_crecer = p.alloc_id();
1458        p.leyes.push(Ley {
1459            id: ley_crecer,
1460            name: "crecer".into(),
1461            kind: LeyKind::Crecer { velocidad: 1.0 },
1462        });
1463
1464        // --- Materiales semilla (arena/pasto/roca/nieve/agua/cactus) ---
1465        let mut mat_ids = std::collections::HashMap::new();
1466        for m in Material::ALL {
1467            let id = p.alloc_id();
1468            let mut def = MaterialDef::from_builtin(id, m);
1469            // El agua fluye; el cactus crece — leyes de muestra (sin simular aún).
1470            match m {
1471                Material::Water => def.leyes.push(LeyUso { ley: ley_fluir, params: vec![1.0, 0.6] }),
1472                Material::Cactus => def.leyes.push(LeyUso { ley: ley_crecer, params: vec![1.0] }),
1473                _ => {}
1474            }
1475            mat_ids.insert(m, id);
1476            p.materiales.push(def);
1477        }
1478        let mid = |m: Material| mat_ids[&m];
1479
1480        // --- Seres ---
1481        let mut sere_ids = Vec::new();
1482        for (name, skin, shirt, pants) in [
1483            ("rojo", [0.90, 0.72, 0.58], [0.82, 0.28, 0.26], [0.20, 0.20, 0.28]),
1484            ("azul", [0.86, 0.68, 0.54], [0.22, 0.55, 0.78], [0.18, 0.20, 0.24]),
1485            ("amarillo", [0.92, 0.78, 0.62], [0.92, 0.80, 0.30], [0.26, 0.22, 0.20]),
1486        ] {
1487            let id = p.alloc_id();
1488            sere_ids.push(id);
1489            p.seres.push(CharSpec {
1490                id,
1491                name: name.into(),
1492                age: Age::Adult,
1493                skin,
1494                shirt,
1495                pants,
1496                hair: None,
1497                cuerpo: None,
1498                conducta: Conducta::default(),
1499                atuendo: Atuendo::default(),
1500                bicho: None,
1501            });
1502        }
1503
1504        // --- Biomas ---
1505        let bioma_desierto = p.alloc_id();
1506        p.biomas.push(Bioma {
1507            id: bioma_desierto,
1508            name: "desierto".into(),
1509            base: 0.30,
1510            dune: 0.05,
1511            relief: 0.45,
1512            mountains: 0.12,
1513            water_level: 0.26,
1514            rivers: 0.18,
1515            peak_at: 1.0,
1516            ground: mid(Material::Sand),
1517            cliff: mid(Material::Rock),
1518            peak: None,
1519            objetos: vec![
1520                crate::worldgen::ObjetoUso { material: mid(Material::Cactus), densidad: 0.010, forma: Forma::Columnar },
1521                // Palmeras ralas (oasis): follaje verde sobre tronco leñoso.
1522                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.004, forma: Forma::Palmera },
1523            ],
1524            minerales: vec![
1525                MineralUso::of(Mineral::Sal),
1526                MineralUso::of(Mineral::Cobre),
1527                MineralUso::of(Mineral::Hierro),
1528                MineralUso::of(Mineral::Oro),
1529                MineralUso::of(Mineral::Rubi),
1530            ],
1531            densidad_urbana: 0.4,
1532            casas: Vec::new(),
1533            fauna: vec![
1534                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Conejo, densidad: 0.25 },
1535                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Serpiente, densidad: 0.3 },
1536                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Zorro, densidad: 0.2 },
1537                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Camello, densidad: 0.2 },
1538                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Lagarto, densidad: 0.3 },
1539                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Escorpion, densidad: 0.25 },
1540            ],
1541            seres: vec![crate::worldgen::SereUso { sere: sere_ids[0], probabilidad: 0.5 }],
1542        });
1543        let bioma_pradera = p.alloc_id();
1544        p.biomas.push(Bioma {
1545            id: bioma_pradera,
1546            name: "pradera".into(),
1547            base: 0.22,
1548            dune: 0.10,
1549            relief: 0.7,
1550            mountains: 0.5,
1551            water_level: 0.30,
1552            rivers: 0.25,
1553            peak_at: 0.80,
1554            ground: mid(Material::Grass),
1555            cliff: mid(Material::Rock),
1556            peak: Some(mid(Material::Snow)),
1557            // Vegetación de pradera: árboles y arbustos ralos sobre un manto de grama
1558            // con flores. El orden importa — la primera forma que brota gana la columna,
1559            // así los árboles no quedan tapados por la grama densa.
1560            objetos: vec![
1561                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.008, forma: Forma::Arbol },
1562                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.012, forma: Forma::Arbusto },
1563                crate::worldgen::ObjetoUso { material: mid(Material::Wheat), densidad: 0.05, forma: Forma::Trigo },
1564                crate::worldgen::ObjetoUso { material: mid(Material::Bloom), densidad: 0.020, forma: Forma::Lavanda },
1565                crate::worldgen::ObjetoUso { material: mid(Material::Bloom), densidad: 0.012, forma: Forma::Flor },
1566                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.010, forma: Forma::Junco },
1567                crate::worldgen::ObjetoUso { material: mid(Material::Grass), densidad: 0.05, forma: Forma::Pasto },
1568            ],
1569            minerales: vec![
1570                MineralUso::of(Mineral::Carbon),
1571                MineralUso::of(Mineral::Hierro),
1572                MineralUso::of(Mineral::Cobre),
1573                MineralUso::of(Mineral::Cuarzo),
1574            ],
1575            densidad_urbana: 0.4,
1576            casas: Vec::new(),
1577            fauna: vec![
1578                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Vaca, densidad: 0.35 },
1579                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Oveja, densidad: 0.35 },
1580                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Conejo, densidad: 0.4 },
1581                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Mariposa, densidad: 0.5 },
1582                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Pato, densidad: 0.3 },
1583                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Cabra, densidad: 0.25 },
1584                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Burro, densidad: 0.15 },
1585            ],
1586            seres: vec![
1587                crate::worldgen::SereUso { sere: sere_ids[1], probabilidad: 0.5 },
1588                crate::worldgen::SereUso { sere: sere_ids[2], probabilidad: 0.4 },
1589            ],
1590        });
1591
1592        let bioma_bosque = p.alloc_id();
1593        p.biomas.push(Bioma {
1594            id: bioma_bosque,
1595            name: "bosque".into(),
1596            base: 0.24,
1597            dune: 0.06,
1598            relief: 0.6,
1599            mountains: 0.35,
1600            water_level: 0.28,
1601            rivers: 0.22,
1602            peak_at: 0.85,
1603            ground: mid(Material::Grass),
1604            cliff: mid(Material::Rock),
1605            peak: Some(mid(Material::Snow)),
1606            // Bosque denso: robles y árboles + coníferas, con tocones, hongos, helechos
1607            // y grama de sotobosque.
1608            objetos: vec![
1609                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.015, forma: Forma::Roble },
1610                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.040, forma: Forma::Arbol },
1611                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.030, forma: Forma::Conifera },
1612                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.025, forma: Forma::Arbusto },
1613                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.006, forma: Forma::Tocon },
1614                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.012, forma: Forma::Hongo },
1615                crate::worldgen::ObjetoUso { material: mid(Material::Cactus), densidad: 0.025, forma: Forma::Helecho },
1616                crate::worldgen::ObjetoUso { material: mid(Material::Grass), densidad: 0.04, forma: Forma::Pasto },
1617            ],
1618            minerales: vec![
1619                MineralUso::of(Mineral::Carbon),
1620                MineralUso::of(Mineral::Plata),
1621                MineralUso::of(Mineral::Amatista),
1622                MineralUso::of(Mineral::Lapis),
1623            ],
1624            densidad_urbana: 0.4,
1625            casas: Vec::new(),
1626            fauna: vec![
1627                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Ciervo, densidad: 0.4 },
1628                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Zorro, densidad: 0.3 },
1629                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Oso, densidad: 0.15 },
1630                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Conejo, densidad: 0.4 },
1631                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Ardilla, densidad: 0.35 },
1632                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Buho, densidad: 0.2 },
1633                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Erizo, densidad: 0.25 },
1634            ],
1635            seres: vec![crate::worldgen::SereUso { sere: sere_ids[1], probabilidad: 0.4 }],
1636        });
1637
1638        let bioma_selva = p.alloc_id();
1639        p.biomas.push(Bioma {
1640            id: bioma_selva,
1641            name: "selva".into(),
1642            base: 0.26,
1643            dune: 0.05,
1644            relief: 0.55,
1645            mountains: 0.30,
1646            water_level: 0.34,
1647            rivers: 0.4,
1648            peak_at: 0.9,
1649            ground: mid(Material::Grass),
1650            cliff: mid(Material::Rock),
1651            peak: None,
1652            // Selva húmeda: dosel de árboles, bambú, plataneras, helechos y hongos.
1653            objetos: vec![
1654                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.050, forma: Forma::Arbol },
1655                crate::worldgen::ObjetoUso { material: mid(Material::Cactus), densidad: 0.050, forma: Forma::Bambu },
1656                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.030, forma: Forma::Platanera },
1657                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.030, forma: Forma::Arbusto },
1658                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.012, forma: Forma::Hongo },
1659                crate::worldgen::ObjetoUso { material: mid(Material::Cactus), densidad: 0.045, forma: Forma::Helecho },
1660                crate::worldgen::ObjetoUso { material: mid(Material::Grass), densidad: 0.04, forma: Forma::Pasto },
1661            ],
1662            minerales: vec![
1663                MineralUso::of(Mineral::Esmeralda),
1664                MineralUso::of(Mineral::Oro),
1665                MineralUso::of(Mineral::Diamante),
1666                MineralUso::of(Mineral::Cobre),
1667            ],
1668            densidad_urbana: 0.4,
1669            casas: Vec::new(),
1670            fauna: vec![
1671                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Rana, densidad: 0.4 },
1672                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Serpiente, densidad: 0.3 },
1673                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Pajaro, densidad: 0.4 },
1674                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Mariposa, densidad: 0.5 },
1675                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Mono, densidad: 0.35 },
1676                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Loro, densidad: 0.35 },
1677                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Tigre, densidad: 0.15 },
1678                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Elefante, densidad: 0.12 },
1679                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Cocodrilo, densidad: 0.15 },
1680            ],
1681            seres: vec![crate::worldgen::SereUso { sere: sere_ids[2], probabilidad: 0.4 }],
1682        });
1683
1684        let bioma_volcan = p.alloc_id();
1685        p.biomas.push(Bioma {
1686            id: bioma_volcan,
1687            name: "volcán".into(),
1688            base: 0.20,
1689            dune: 0.04,
1690            relief: 0.9,
1691            mountains: 0.85,
1692            water_level: 0.0,
1693            rivers: 0.0,
1694            peak_at: 0.72,
1695            // La tanda nueva de materiales: laderas de basalto, cumbre de lava.
1696            ground: mid(Material::Basalto),
1697            cliff: mid(Material::Basalto),
1698            peak: Some(mid(Material::Lava)),
1699            objetos: vec![],
1700            // Veta rica: el fuego trae metales, gemas rojas y exóticos volcánicos.
1701            minerales: vec![
1702                MineralUso::of(Mineral::Hierro),
1703                MineralUso::of(Mineral::Oro),
1704                MineralUso::of(Mineral::Rubi),
1705                MineralUso::of(Mineral::Obsidiana),
1706                MineralUso::of(Mineral::Cinabrio),
1707            ],
1708            densidad_urbana: 0.4,
1709            casas: Vec::new(),
1710            fauna: vec![],
1711            seres: vec![],
1712        });
1713
1714        let bioma_gruta = p.alloc_id();
1715        p.biomas.push(Bioma {
1716            id: bioma_gruta,
1717            name: "gruta de cristales".into(),
1718            base: 0.30,
1719            dune: 0.05,
1720            relief: 0.8,
1721            mountains: 0.7,
1722            water_level: 0.10,
1723            rivers: 0.05,
1724            peak_at: 0.9,
1725            // Roca clara (granito) para que resalten las gemas exóticas.
1726            ground: mid(Material::Granito),
1727            cliff: mid(Material::Granito),
1728            peak: None,
1729            objetos: vec![],
1730            // Vitrina de exóticos: geodas, cristales y vetas de colores raros.
1731            minerales: vec![
1732                MineralUso::of(Mineral::Amatista),
1733                MineralUso::of(Mineral::Zafiro),
1734                MineralUso::of(Mineral::Topacio),
1735                MineralUso::of(Mineral::Turquesa),
1736                MineralUso::of(Mineral::Opalo),
1737                MineralUso::of(Mineral::Fluorita),
1738                MineralUso::of(Mineral::Jade),
1739                MineralUso::of(Mineral::Bismuto),
1740                MineralUso::of(Mineral::Uraninita),
1741            ],
1742            densidad_urbana: 0.4,
1743            casas: Vec::new(),
1744            fauna: vec![],
1745            seres: vec![],
1746        });
1747
1748        let bioma_pueblo = p.alloc_id();
1749        p.biomas.push(Bioma {
1750            id: bioma_pueblo,
1751            name: "pueblo".into(),
1752            base: 0.16,
1753            dune: 0.03,
1754            relief: 0.22,
1755            mountains: 0.12,
1756            water_level: 0.0,
1757            rivers: 0.0,
1758            peak_at: 1.0,
1759            ground: mid(Material::Grass),
1760            cliff: mid(Material::Rock),
1761            peak: None,
1762            objetos: vec![
1763                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.006, forma: Forma::Arbol },
1764                crate::worldgen::ObjetoUso { material: mid(Material::Grass), densidad: 0.04, forma: Forma::Pasto },
1765            ],
1766            minerales: vec![],
1767            // Un pueblo mezclado, esparcido por la semilla del mundo.
1768            densidad_urbana: 0.55,
1769            casas: vec![
1770                CasaUso { tipo: CasaTipo::Cabana, densidad: 0.35 },
1771                CasaUso { tipo: CasaTipo::Moderna, densidad: 0.3 },
1772                CasaUso { tipo: CasaTipo::Flintstone, densidad: 0.2 },
1773                CasaUso { tipo: CasaTipo::Hobbit, densidad: 0.2 },
1774                CasaUso { tipo: CasaTipo::Edificio, densidad: 0.15 },
1775            ],
1776            fauna: vec![
1777                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Gallina, densidad: 0.5 },
1778                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Cerdo, densidad: 0.3 },
1779                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Vaca, densidad: 0.3 },
1780                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Pato, densidad: 0.3 },
1781                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Cabra, densidad: 0.2 },
1782                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Burro, densidad: 0.15 },
1783            ],
1784            seres: vec![],
1785        });
1786
1787        // La costa: el suelo a la altura del agua, así las lomas de arena hacen
1788        // playa e islas y lo bajo es mar — «está linda la mar».
1789        let bioma_mar = p.alloc_id();
1790        p.biomas.push(Bioma {
1791            id: bioma_mar,
1792            name: "mar".into(),
1793            base: 0.295,
1794            dune: 0.12,
1795            relief: 0.18,
1796            mountains: 0.08,
1797            water_level: 0.30,
1798            rivers: 0.0,
1799            peak_at: 1.0,
1800            ground: mid(Material::Sand),
1801            cliff: mid(Material::Arenisca),
1802            peak: None,
1803            objetos: vec![
1804                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.010, forma: Forma::Palmera },
1805                crate::worldgen::ObjetoUso { material: mid(Material::Grass), densidad: 0.015, forma: Forma::Junco },
1806            ],
1807            minerales: vec![],
1808            densidad_urbana: 0.0,
1809            casas: Vec::new(),
1810            fauna: vec![
1811                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Cangrejo, densidad: 0.3 },
1812                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Pelicano, densidad: 0.2 },
1813                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Tortuga, densidad: 0.15 },
1814            ],
1815            seres: vec![],
1816        });
1817
1818        // La luna: polvo gris, lomas suaves, ni agua ni nada que crezca. Va con
1819        // el cielo de noche de la escena (`SceneSpec::cielo`).
1820        let bioma_luna = p.alloc_id();
1821        p.biomas.push(Bioma {
1822            id: bioma_luna,
1823            name: "luna".into(),
1824            base: 0.22,
1825            dune: 0.06,
1826            relief: 0.30,
1827            mountains: 0.35,
1828            water_level: 0.0,
1829            rivers: 0.0,
1830            peak_at: 1.0,
1831            ground: mid(Material::Grava),
1832            cliff: mid(Material::Basalto),
1833            peak: None,
1834            objetos: vec![],
1835            minerales: vec![],
1836            densidad_urbana: 0.0,
1837            casas: Vec::new(),
1838            fauna: vec![],
1839            seres: vec![],
1840        });
1841
1842        // --- Mundos ---
1843        let mundo_desierto = p.alloc_id();
1844        p.mundos.push(Mundo {
1845            id: mundo_desierto,
1846            name: "desierto".into(),
1847            seed: 1337,
1848            biomas: vec![bioma_desierto],
1849            mapa: None,
1850        });
1851        let mundo_pradera = p.alloc_id();
1852        p.mundos.push(Mundo {
1853            id: mundo_pradera,
1854            name: "pradera".into(),
1855            seed: 1337,
1856            biomas: vec![bioma_pradera],
1857            mapa: None,
1858        });
1859        let mundo_bosque = p.alloc_id();
1860        p.mundos.push(Mundo {
1861            id: mundo_bosque,
1862            name: "bosque".into(),
1863            seed: 2027,
1864            biomas: vec![bioma_bosque],
1865            mapa: None,
1866        });
1867        let mundo_selva = p.alloc_id();
1868        p.mundos.push(Mundo {
1869            id: mundo_selva,
1870            name: "selva".into(),
1871            seed: 3141,
1872            biomas: vec![bioma_selva],
1873            mapa: None,
1874        });
1875        let mundo_volcan = p.alloc_id();
1876        p.mundos.push(Mundo {
1877            id: mundo_volcan,
1878            name: "volcán".into(),
1879            seed: 777,
1880            biomas: vec![bioma_volcan],
1881            mapa: None,
1882        });
1883        let mundo_gruta = p.alloc_id();
1884        p.mundos.push(Mundo {
1885            id: mundo_gruta,
1886            name: "gruta de cristales".into(),
1887            seed: 2718,
1888            biomas: vec![bioma_gruta],
1889            mapa: None,
1890        });
1891        let mundo_pueblo = p.alloc_id();
1892        p.mundos.push(Mundo {
1893            id: mundo_pueblo,
1894            name: "pueblo".into(),
1895            seed: 1234,
1896            biomas: vec![bioma_pueblo],
1897            mapa: None,
1898        });
1899        let mundo_mar = p.alloc_id();
1900        p.mundos.push(Mundo { id: mundo_mar, name: "mar".into(), seed: 1916, biomas: vec![bioma_mar], mapa: None });
1901        let mundo_luna = p.alloc_id();
1902        p.mundos.push(Mundo { id: mundo_luna, name: "luna".into(), seed: 1969, biomas: vec![bioma_luna], mapa: None });
1903
1904        // --- Escena demo ---
1905        let dim = world_dim(PREVIEW_DIM_XZ);
1906        let mut escena = SceneSpec::walk_and_emote("saludo en el desierto", mundo_desierto, 3, Clip::Wave, dim);
1907        escena.id = p.alloc_id();
1908        p.escenas.push(escena);
1909
1910        // --- Interior demo (sala amueblada) ---
1911        let mut sala = crate::interior::Interior::nuevo(p.alloc_id(), "sala");
1912        sala.colocar(crate::Mueble::Alfombra, 12, 10, 0);
1913        sala.colocar(crate::Mueble::Sofa, 12, 6, 0);
1914        sala.colocar(crate::Mueble::Mesa, 12, 10, 0);
1915        sala.colocar(crate::Mueble::Silla, 9, 10, 1);
1916        sala.colocar(crate::Mueble::Estante, 4, 3, 0);
1917        sala.colocar(crate::Mueble::Lampara, 20, 4, 0);
1918        sala.colocar(crate::Mueble::Maceta, 4, 16, 0);
1919        sala.colocar(crate::Mueble::Ficus, 20, 16, 0);
1920        sala.colocar(crate::Mueble::Cuadro, 12, 1, 0);
1921        sala.colocar(crate::Mueble::Espejo, 8, 1, 0);
1922        sala.colocar(crate::Mueble::Colgante, 16, 2, 0);
1923        sala.colocar(crate::Mueble::Aplique, 1, 8, 1);
1924        p.interiores.push(sala);
1925
1926        p
1927    }
1928
1929    // --- Búsquedas por id ---
1930    pub fn ley(&self, id: u64) -> Option<&Ley> {
1931        self.leyes.iter().find(|x| x.id == id)
1932    }
1933    pub fn material(&self, id: u64) -> Option<&MaterialDef> {
1934        self.materiales.iter().find(|x| x.id == id)
1935    }
1936    pub fn sere(&self, id: u64) -> Option<&CharSpec> {
1937        self.seres.iter().find(|x| x.id == id)
1938    }
1939    pub fn bioma(&self, id: u64) -> Option<&Bioma> {
1940        self.biomas.iter().find(|x| x.id == id)
1941    }
1942    pub fn mundo(&self, id: u64) -> Option<&Mundo> {
1943        self.mundos.iter().find(|x| x.id == id)
1944    }
1945
1946    /// Id del material de fábrica de la variante `m`, si está sembrado (para que la
1947    /// IA/presets encuentren «la arena» sin clavar números).
1948    pub fn material_id_for(&self, m: Material) -> Option<u64> {
1949        self.materiales.iter().find(|d| d.builtin == Some(m)).map(|d| d.id)
1950    }
1951
1952    /// **Resuelve un material** aplanando la cadena de herencia: el primer `Some`
1953    /// hacia arriba gana; si nada lo define, cae al color/grano del `builtin`, y por
1954    /// último a un gris neutro. Corta a profundidad 32 (anti-ciclo).
1955    pub fn resolve_material(&self, id: u64) -> ResolvedMaterial {
1956        let mut color: Option<[f32; 3]> = None;
1957        let mut grain: Option<f32> = None;
1958        let mut brillo: Option<f32> = None;
1959        let mut builtin: Option<Material> = None;
1960        let mut cur = Some(id);
1961        let mut depth = 0;
1962        while let Some(cid) = cur {
1963            if depth > 32 {
1964                break;
1965            }
1966            let Some(m) = self.material(cid) else { break };
1967            if color.is_none() {
1968                color = m.color;
1969            }
1970            if grain.is_none() {
1971                grain = m.grain;
1972            }
1973            if brillo.is_none() {
1974                brillo = m.brillo;
1975            }
1976            if builtin.is_none() {
1977                builtin = m.builtin;
1978            }
1979            cur = m.parent;
1980            depth += 1;
1981        }
1982        let color_u8 = color
1983            .map(|c| {
1984                [
1985                    (c[0].clamp(0.0, 1.0) * 255.0) as u8,
1986                    (c[1].clamp(0.0, 1.0) * 255.0) as u8,
1987                    (c[2].clamp(0.0, 1.0) * 255.0) as u8,
1988                ]
1989            })
1990            .or_else(|| builtin.map(|b| b.color()))
1991            .unwrap_or([150, 150, 150]);
1992        let grain = grain.or_else(|| builtin.map(|b| b.grain())).unwrap_or(0.0);
1993        let brillo = brillo.or_else(|| builtin.map(|b| b.brillo())).unwrap_or(0.0);
1994        // El detalle sub-voxel (rugosidad/bisel) viaja POR VOXEL con el material, así
1995        // dos materiales del bioma se ven distinto a la vez (lo que el look global no
1996        // podía). Aplana la herencia igual que color/brillo (`detalle_material`).
1997        ResolvedMaterial::new_b(color_u8, grain, brillo).con_detalle(self.detalle_material(id))
1998    }
1999
2000    /// **Detalle sub-voxel resuelto** `(rugosidad, bisel)` de un material, aplanando
2001    /// la herencia (primer `Some` hacia arriba gana; default `0.0` = liso/duro). Es un
2002    /// look GLOBAL del ray-march (no hay id de material por voxel todavía), así que el
2003    /// preview aplica el del material en foco. Espeja [`Self::resolve_material`].
2004    pub fn detalle_material(&self, id: u64) -> (f32, f32) {
2005        let mut rugosidad: Option<f32> = None;
2006        let mut bisel: Option<f32> = None;
2007        let mut cur = Some(id);
2008        let mut depth = 0;
2009        while let Some(cid) = cur {
2010            if depth > 32 {
2011                break;
2012            }
2013            let Some(m) = self.material(cid) else { break };
2014            if rugosidad.is_none() {
2015                rugosidad = m.rugosidad;
2016            }
2017            if bisel.is_none() {
2018                bisel = m.bisel;
2019            }
2020            cur = m.parent;
2021            depth += 1;
2022        }
2023        (rugosidad.unwrap_or(0.0).max(0.0), bisel.unwrap_or(0.0).clamp(0.0, 1.0))
2024    }
2025
2026    /// **Leyes efectivas** de un material: las propias + las heredadas de sus padres,
2027    /// cada una con sus parámetros aplicados (la `LeyUso.params` overridea los
2028    /// defaults de la ley). Es lo que dispara la simulación: un material *fluye*
2029    /// porque tiene una [`LeyKind::Fluir`], *crece* porque tiene [`LeyKind::Crecer`].
2030    pub fn resolve_laws(&self, material_id: u64) -> Vec<LeyKind> {
2031        let mut out = Vec::new();
2032        let mut cur = Some(material_id);
2033        let mut depth = 0;
2034        while let Some(cid) = cur {
2035            if depth > 32 {
2036                break;
2037            }
2038            let Some(m) = self.material(cid) else { break };
2039            for u in &m.leyes {
2040                if let Some(ley) = self.ley(u.ley) {
2041                    let mut kind = ley.kind.clone();
2042                    for (i, &v) in u.params.iter().enumerate() {
2043                        kind.set_param(i, v);
2044                    }
2045                    out.push(kind);
2046                }
2047            }
2048            cur = m.parent;
2049            depth += 1;
2050        }
2051        out
2052    }
2053
2054    /// Si el material tiene una ley [`Fluir`](LeyKind::Fluir), sus parámetros
2055    /// `(gravedad, horizontal)` — lo que vuelve líquido a un material. `None` = no fluye.
2056    pub fn fluir_params(&self, material_id: u64) -> Option<(f32, f32)> {
2057        self.resolve_laws(material_id).into_iter().find_map(|k| match k {
2058            LeyKind::Fluir { gravedad, horizontal } => Some((gravedad, horizontal)),
2059            _ => None,
2060        })
2061    }
2062
2063    /// Si el material tiene una ley [`Crecer`](LeyKind::Crecer), su `velocidad`.
2064    /// `None` = no crece.
2065    pub fn crecer_velocidad(&self, material_id: u64) -> Option<f32> {
2066        self.resolve_laws(material_id).into_iter().find_map(|k| match k {
2067            LeyKind::Crecer { velocidad } => Some(velocidad),
2068            _ => None,
2069        })
2070    }
2071
2072    /// Parámetros de Fluir del **agua** (el material semilla `Water`), si los tiene.
2073    /// El studio lo usa para que el agua del bioma fluya según su ley.
2074    pub fn water_fluir(&self) -> Option<(f32, f32)> {
2075        self.fluir_params(self.material_id_for(Material::Water)?)
2076    }
2077
2078    /// Si el material tiene una ley [`Ecuacion`](LeyKind::Ecuacion) (con herencia), su
2079    /// `(campos, programa compilado, valores de parámetros)` — listo para correr con
2080    /// [`EcuacionSim`](crate::EcuacionSim) sobre las celdas del material en el preview.
2081    /// `None` si no tiene una, o si su fórmula no compila.
2082    pub fn ecuacion_de_material(
2083        &self,
2084        material_id: u64,
2085    ) -> Option<(Vec<FieldDef>, Program, Vec<f32>)> {
2086        self.resolve_laws(material_id).into_iter().find_map(|k| match &k {
2087            LeyKind::Ecuacion { campos, .. } => {
2088                let prog = k.compile_ecuacion()?.ok()?;
2089                Some((campos.clone(), prog, k.ecuacion_param_values()))
2090            }
2091            _ => None,
2092        })
2093    }
2094
2095    /// Arma la [`BiomaPalette`] resuelta de un bioma (colores concretos para el render).
2096    pub fn bioma_palette(&self, b: &Bioma) -> BiomaPalette {
2097        let agua = self
2098            .material_id_for(Material::Water)
2099            .map(|id| self.resolve_material(id).color)
2100            .unwrap_or_else(|| Material::Water.color());
2101        BiomaPalette {
2102            ground: self.resolve_material(b.ground),
2103            cliff: self.resolve_material(b.cliff),
2104            peak: b.peak.map(|id| self.resolve_material(id)),
2105            agua,
2106            objetos: b
2107                .objetos
2108                .iter()
2109                .map(|o| (self.resolve_material(o.material), o.densidad, o.forma))
2110                .collect(),
2111        }
2112    }
2113
2114    /// Todo lo que el render necesita de un mundo: su primer bioma (relieve), la
2115    /// semilla y la paleta resuelta. `None` si el mundo o su bioma no existen.
2116    pub fn render_mundo(&self, mundo_id: u64) -> Option<MundoRender> {
2117        let m = self.mundo(mundo_id)?;
2118        let bid = *m.biomas.first()?;
2119        let bioma = self.bioma(bid)?.clone();
2120        let palette = self.bioma_palette(&bioma);
2121        Some(MundoRender { bioma, seed: m.seed, palette })
2122    }
2123
2124    /// El render de un **bioma** suelto (modo Biomas), con una semilla fija de
2125    /// previsualización. `None` si el bioma no existe.
2126    pub fn render_bioma(&self, bioma_id: u64) -> Option<MundoRender> {
2127        let bioma = self.bioma(bioma_id)?.clone();
2128        let palette = self.bioma_palette(&bioma);
2129        Some(MundoRender { bioma, seed: 1337, palette })
2130    }
2131
2132    /// Ser `i` (por índice, como referencian los actores de una escena), o uno por
2133    /// defecto si el índice se sale.
2134    pub fn character_or_default(&self, i: usize) -> CharSpec {
2135        self.seres
2136            .get(i)
2137            .cloned()
2138            .unwrap_or_else(|| CharSpec::new("actor", Age::Adult))
2139    }
2140}
2141
2142#[cfg(test)]
2143mod tests {
2144    use super::*;
2145
2146    /// Una bandada de `n` humanoides en fila, alejándose del ojo en Z.
2147    fn bandada(n: usize) -> Vec<CharSpec> {
2148        (0..n).map(|i| CharSpec::new(format!("ser{i}"), Age::Adult)).collect()
2149    }
2150
2151    fn en_cuadro<'a>(specs: &'a [CharSpec], paso: f32) -> Vec<EnCuadro<'a>> {
2152        specs
2153            .iter()
2154            .enumerate()
2155            .map(|(i, spec)| EnCuadro {
2156                spec,
2157                pos: Vec3::new(0.0, 0.0, i as f32 * paso),
2158                yaw: 0.0,
2159                clip: Clip::Walk,
2160                fase: i as f32 * 0.3,
2161            })
2162            .collect()
2163    }
2164
2165    /// El rey del cuento: corona, manto largo, barba entera, pelo largo y anteojos
2166    /// —lo más caro que se le puede poner a un ser—.
2167    fn rey() -> CharSpec {
2168        use crate::ser_sdf::{Peinado, Sombrero};
2169        let mut c = CharSpec::new("rey", Age::Elder);
2170        c.atuendo = Atuendo {
2171            peinado: Peinado::Larga,
2172            barba: 1.0,
2173            sombrero: Sombrero::Corona,
2174            anteojos: true,
2175            falda: 0.9,
2176            manto: 1.0,
2177            ..Atuendo::default()
2178        };
2179        c
2180    }
2181
2182    #[test]
2183    fn el_atuendo_viaja_en_el_documento_y_uno_viejo_abre_desnudo() {
2184        // Un ser escrito antes de que existiera el vestuario abre sin ropa.
2185        let viejo = r#"(name: "a", age: Adult, skin: (0.9, 0.7, 0.6),
2186            shirt: (0.8, 0.3, 0.3), pants: (0.2, 0.2, 0.3))"#;
2187        let v: CharSpec = ron::from_str(viejo).unwrap();
2188        assert!(v.atuendo.es_default());
2189        // Y por postcard, el formato nativo, el vestido va y vuelve.
2190        let bytes = postcard::to_allocvec(&rey()).unwrap();
2191        let de_postcard: CharSpec = postcard::from_bytes(&bytes).unwrap();
2192        assert_eq!(de_postcard.atuendo, rey().atuendo);
2193        // Y uno vestido va y vuelve igual.
2194        let r = rey();
2195        let txt = ron::to_string(&r).unwrap();
2196        let vuelta: CharSpec = ron::from_str(&txt).unwrap();
2197        assert_eq!(vuelta.atuendo, r.atuendo);
2198        // A mano se nombra sólo lo que se le pone; lo demás es default.
2199        let escrito = r#"(name: "princesa", age: Child, skin: (0.9, 0.75, 0.62),
2200            shirt: (0.95, 0.80, 0.86), pants: (0.3, 0.3, 0.4),
2201            atuendo: (peinado: Larga, falda: 1.0, sombrero: Corona))"#;
2202        let p: CharSpec = ron::from_str(escrito).unwrap();
2203        assert_eq!(p.atuendo.falda, 1.0);
2204        assert_eq!(p.atuendo.manto, 0.0);
2205        assert_eq!(p.atuendo.sombrero, crate::ser_sdf::Sombrero::Corona);
2206    }
2207
2208    #[test]
2209    fn el_ser_que_filma_la_escena_lleva_puesto_su_atuendo() {
2210        // Brazo de control: el mismo ser, desnudo, en el MISMO camino (`to_sdf_lod`,
2211        // el que usan los repartos y por ende `ende`).
2212        let vestido = rey();
2213        let desnudo = CharSpec { atuendo: Atuendo::default(), ..vestido.clone() };
2214        for d in [crate::DetalleSer::Hero, crate::DetalleSer::Medio, crate::DetalleSer::Lejos] {
2215            let n = |c: &CharSpec| c.to_sdf_lod(Clip::Idle, 0.0, Vec3::ZERO, 0.0, None, d).prims.len();
2216            assert!(n(&vestido) > n(&desnudo), "{d:?}: {} vs {}", n(&vestido), n(&desnudo));
2217            assert_eq!(n(&vestido) - n(&desnudo), vestido.atuendo.costo_extra(d), "{d:?}");
2218            // El costo es COTA del ser real: es lo único que el reparto mira antes
2219            // de armarlo. Con la cuenta desnuda, el rey costaba lo de un ser sin ropa.
2220            for fase in [0.0, 0.7, 1.9, 3.3] {
2221                let real = vestido.to_sdf_lod(Clip::Walk, fase, Vec3::ZERO, 0.0, None, d).prims.len();
2222                assert!(real <= vestido.costo(d), "{d:?} fase {fase}: {real} > {}", vestido.costo(d));
2223            }
2224        }
2225        assert_eq!(desnudo.costo(crate::DetalleSer::Hero), crate::DetalleSer::Hero.costo());
2226    }
2227
2228    #[test]
2229    fn un_reparto_de_seres_vestidos_no_se_pasa_del_uniform() {
2230        use llimphi_3d::{VOXEL_MAX_SERS, VOXEL_MAX_SER_PRIMS};
2231        // El techo por detalle se calibró sin ropa: con la cuenta vieja, una bandada
2232        // de reyes se pasaba y el empacador les cortaba la cola en silencio.
2233        // Se barren separaciones: el desborde pasa sólo cuando el último admitido cae
2234        // justo en el borde del presupuesto, y con un solo caso eso es suerte.
2235        let specs: Vec<CharSpec> = (0..VOXEL_MAX_SERS * 2).map(|_| rey()).collect();
2236        let ojo = Vec3::new(0.0, 1.6, -4.0);
2237        for paso in [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.5, 6.0, 9.0] {
2238            let sers = reparto_sdf(&en_cuadro(&specs, paso), ojo, true);
2239            let total: usize = sers.iter().map(|s| s.prims.len()).sum();
2240            assert!(total <= VOXEL_MAX_SER_PRIMS, "reparto_sdf paso {paso}: {total} primitivas");
2241            let rep = reparto_mixto(&en_cuadro(&specs, paso), ojo, true, Vec3::new(64.0, 32.0, 64.0));
2242            let total: usize = rep.sers.iter().map(|s| s.prims.len()).sum();
2243            assert!(total <= VOXEL_MAX_SER_PRIMS, "reparto_mixto paso {paso}: {total} primitivas");
2244            assert_eq!(rep.sers.len() + rep.mallas.len(), specs.len(), "nadie se cae del cuadro");
2245        }
2246    }
2247
2248    fn elefante() -> CharSpec {
2249        let mut c = CharSpec::new("elefante", Age::Adult);
2250        c.bicho = Some(Bicho::Elefante);
2251        c
2252    }
2253
2254    #[test]
2255    fn el_elefante_es_un_actor_que_se_filma_con_su_cuerpo_del_bestiario() {
2256        use crate::bicho_receta::alto_tipico;
2257        let e = elefante();
2258        assert!(e.es_sdf(), "el elefante va por el ray-marcher, no por cajas");
2259        assert_eq!(e.altura(), alto_tipico(Bicho::Elefante));
2260        // Es el MISMO cuerpo que el de la fauna, posado donde dice el guion.
2261        let pos = Vec3::new(10.0, 4.0, 20.0);
2262        let ser = e.to_sdf_lod(Clip::Idle, 0.0, pos, 0.7, Some(Vec3::ZERO), crate::DetalleSer::Hero);
2263        let fauna = crate::bicho_sdf_lod(
2264            Bicho::Elefante,
2265            alto_tipico(Bicho::Elefante),
2266            crate::Andanza::Quieto,
2267            0.0,
2268            crate::DetalleSer::Hero,
2269        );
2270        assert_eq!(ser.prims.len(), fauna.prims.len());
2271        assert_eq!(ser.pos, [pos.x, pos.y, pos.z]);
2272        assert_eq!(ser.yaw, 0.7);
2273        // Camina: dos instantes del paso no son la misma pose.
2274        let a = e.to_sdf(Clip::Walk, 0.0);
2275        let b = e.to_sdf(Clip::Walk, 0.4);
2276        assert_ne!(format!("{:?}", a.prims), format!("{:?}", b.prims), "el paso no se mueve");
2277        // La cuenta del reparto cubre lo que de verdad se arma, en cada clip y detalle.
2278        for d in [crate::DetalleSer::Hero, crate::DetalleSer::Medio, crate::DetalleSer::Lejos] {
2279            for c in [Clip::Idle, Clip::Walk, Clip::Run, Clip::Wave, Clip::Point, Clip::Cheer] {
2280                let n = e.to_sdf_lod(c, 1.3, pos, 0.0, None, d).prims.len();
2281                assert!(n <= e.costo(d), "{d:?}/{c:?}: arma {n} y declara {}", e.costo(d));
2282            }
2283        }
2284        // Cuando cae a cajas, la silueta mide lo que mide el elefante.
2285        let (model, v, _) = e.to_meta(Vec3::ZERO, 0.0, Clip::Walk, 0.0, None);
2286        let alto = v
2287            .iter()
2288            .map(|x| model.transform_point3(Vec3::from_array(x.pos)).y)
2289            .fold(0.0f32, f32::max);
2290        assert!((alto - e.altura()).abs() < 0.05, "la malla de respaldo mide {alto}");
2291    }
2292
2293    #[test]
2294    fn el_rey_y_su_manada_de_elefantes_entran_en_el_cuadro() {
2295        use llimphi_3d::VOXEL_MAX_SER_PRIMS;
2296        // «un rebaño de elefantes»: el rey y ocho, barriendo separaciones como en el
2297        // reparto de los vestidos. Nadie se cae del cuadro y el uniform no desborda.
2298        let mut specs = vec![rey()];
2299        specs.extend((0..8).map(|_| elefante()));
2300        let ojo = Vec3::new(0.0, 1.6, -6.0);
2301        for paso in [0.0, 1.0, 2.5, 4.0, 7.0] {
2302            let sers = reparto_sdf(&en_cuadro(&specs, paso), ojo, true);
2303            let total: usize = sers.iter().map(|s| s.prims.len()).sum();
2304            assert!(total <= VOXEL_MAX_SER_PRIMS, "reparto_sdf paso {paso}: {total}");
2305            let rep = reparto_mixto(&en_cuadro(&specs, paso), ojo, true, Vec3::new(64.0, 32.0, 64.0));
2306            let total: usize = rep.sers.iter().map(|s| s.prims.len()).sum();
2307            assert!(total <= VOXEL_MAX_SER_PRIMS, "reparto_mixto paso {paso}: {total}");
2308            assert_eq!(rep.sers.len() + rep.mallas.len(), specs.len(), "nadie se cae del cuadro");
2309        }
2310    }
2311
2312    /// La escena de «El prendedor»: la princesa con la estrella en la mano y la flor,
2313    /// la pluma y la perla en el mundo.
2314    fn escena_del_prendedor() -> SceneSpec {
2315        use crate::{Utileria, UtileriaSpec};
2316        let mut s = SceneSpec::walk_and_emote("prendedor", 0, 2, Clip::Wave, [96, 24, 96]);
2317        let mut estrella = UtileriaSpec::en(Utileria::Estrella, [0.0; 3]);
2318        estrella.lleva = Some(0);
2319        estrella.desde = 1.0;
2320        s.utileria.push(estrella);
2321        for (i, u) in [Utileria::Flor, Utileria::Pluma, Utileria::Perla].into_iter().enumerate() {
2322            s.utileria.push(UtileriaSpec::en(u, [40.0 + i as f32, 10.0, 40.0]));
2323        }
2324        s
2325    }
2326
2327    #[test]
2328    fn la_utileria_se_reserva_su_lugar_y_el_reparto_se_reparte_el_resto() {
2329        use llimphi_3d::{VOXEL_MAX_SERS, VOXEL_MAX_SER_PRIMS};
2330        let escena = escena_del_prendedor();
2331        let specs: Vec<CharSpec> = (0..VOXEL_MAX_SERS * 2).map(|_| rey()).collect();
2332        let ojo = Vec3::new(0.0, 1.6, -4.0);
2333        let mano = [(Vec3::new(30.0, 10.0, 30.0), 0.0, Vec3::new(0.3, 0.9, 0.2))];
2334        assert!(escena.utileria_en(0.5, &mano).len() == 3, "la estrella aún no apareció");
2335        let utileria = escena.utileria_en(2.0, &mano);
2336        assert_eq!(utileria.len(), 4, "las cuatro piezas en cuadro");
2337        for paso in [0.0, 0.5, 1.0, 2.0, 4.5, 9.0] {
2338            let rep = reparto_mixto_con(&en_cuadro(&specs, paso), ojo, true, Vec3::new(64.0, 32.0, 64.0), &utileria);
2339            let total: usize =
2340                utileria.iter().chain(&rep.sers).map(|s| s.prims.len()).sum();
2341            assert!(total <= VOXEL_MAX_SER_PRIMS, "paso {paso}: {total} primitivas con la utilería");
2342            assert!(utileria.len() + rep.sers.len() <= VOXEL_MAX_SERS, "paso {paso}: más seres que lugares");
2343            assert_eq!(rep.sers.len() + rep.mallas.len(), specs.len(), "nadie se cae del cuadro");
2344        }
2345        // Sin utilería, el reparto con reserva es el de siempre.
2346        let a = reparto_mixto(&en_cuadro(&specs, 1.0), ojo, true, Vec3::splat(64.0));
2347        let b = reparto_mixto_con(&en_cuadro(&specs, 1.0), ojo, true, Vec3::splat(64.0), &[]);
2348        assert_eq!((a.sers.len(), a.mallas.len()), (b.sers.len(), b.mallas.len()));
2349    }
2350
2351    #[test]
2352    fn la_estrella_sube_cuando_la_princesa_saluda() {
2353        use crate::{Utileria, UtileriaSpec};
2354        let princesa = CharSpec::new("princesa", Age::Child);
2355        let mut s = SceneSpec::walk_and_emote("estrella", 0, 1, Clip::Wave, [96, 24, 96]);
2356        let mut e = UtileriaSpec::en(Utileria::Estrella, [0.0; 3]);
2357        e.lleva = Some(0);
2358        s.utileria.push(e);
2359        let pies = Vec3::new(40.0, 10.0, 40.0);
2360        let en = |clip: Clip| {
2361            let u = s.utileria_en(1.0, &[(pies, 0.3, princesa.mano(clip, 1.2))]);
2362            Vec3::from_array(u[0].pos)
2363        };
2364        let quieta = en(Clip::Idle);
2365        let saludando = en(Clip::Wave);
2366        assert!(saludando.y > quieta.y + 0.2, "la estrella no subió: {quieta} → {saludando}");
2367        // Y está donde la mano que se dibuja: la del ser filmado, posado igual.
2368        let ser = princesa.to_sdf_lod(Clip::Wave, 1.2, pies, 0.3, None, crate::DetalleSer::Hero);
2369        let manos: Vec<Vec3> = ser
2370            .prims
2371            .iter()
2372            .filter(|p| p.kind == llimphi_3d::SdfKind::Esfera && p.color == princesa.skin)
2373            .map(|p| pies + crate::sdf_kit::rot_y(Vec3::from_array(p.a), 0.3))
2374            .collect();
2375        assert!(
2376            manos.iter().any(|m| m.distance(saludando) < 1e-4),
2377            "la estrella no está en ninguna de las manos dibujadas"
2378        );
2379        // El elefante la lleva sobre el lomo.
2380        let mut el = CharSpec::new("elefante", Age::Adult);
2381        el.bicho = Some(Bicho::Elefante);
2382        assert_eq!(el.mano(Clip::Walk, 0.0).y, el.altura());
2383    }
2384
2385    #[test]
2386    fn cien_perlas_no_le_quitan_la_cara_al_reparto() {
2387        use crate::{Utileria, UtileriaSpec};
2388        use llimphi_3d::{VOXEL_MAX_SERS, VOXEL_MAX_SER_PRIMS};
2389        let mut s = SceneSpec::walk_and_emote("perlas", 0, 1, Clip::Idle, [96, 24, 96]);
2390        s.utileria = (0..100).map(|i| UtileriaSpec::en(Utileria::Estrella, [i as f32, 10.0, 0.0])).collect();
2391        let u = s.utileria_en(1.0, &[]);
2392        assert!(!u.is_empty());
2393        assert!(u.len() <= VOXEL_MAX_SERS / 4);
2394        assert!(u.iter().map(|x| x.prims.len()).sum::<usize>() <= VOXEL_MAX_SER_PRIMS / 4);
2395    }
2396
2397    #[test]
2398    fn la_utileria_viaja_en_el_documento_y_una_escena_vieja_abre_sin_ella() {
2399        let e = escena_del_prendedor();
2400        let vuelta: SceneSpec = ron::from_str(&ron::to_string(&e).unwrap()).unwrap();
2401        assert_eq!(vuelta.utileria, e.utileria);
2402        let bytes = postcard::to_allocvec(&e).unwrap();
2403        let de_postcard: SceneSpec = postcard::from_bytes(&bytes).unwrap();
2404        assert_eq!(de_postcard.utileria, e.utileria);
2405        // Escrita a mano, como la escribe un guion: sólo lo que importa.
2406        let txt = r#"(name: "a", mundo: 0, duration: 4.0, actors: [],
2407            utileria: [(cosa: Estrella, pos: (10.0, 20.0, 30.0), desde: 1.5, giro: 0.1)])"#;
2408        let a_mano: SceneSpec = ron::from_str(txt).unwrap();
2409        assert_eq!(a_mano.utileria[0].cosa, crate::Utileria::Estrella);
2410        assert_eq!(a_mano.utileria[0].lleva, None);
2411        let vieja: SceneSpec = ron::from_str(r#"(name: "a", mundo: 0, duration: 4.0, actors: [])"#).unwrap();
2412        assert!(vieja.utileria.is_empty());
2413    }
2414
2415    /// **La estrella alumbra de noche, y sólo de noche**: con cielo de noche la
2416    /// estrella en la mano da una luz centrada en ella, que crece mientras la
2417    /// estrella aparece; de día no da ninguna. Y es la única pieza con luz propia
2418    /// (el shader la lee de un canal de color > 1,5).
2419    #[test]
2420    fn la_estrella_alumbra_de_noche_y_solo_de_noche() {
2421        let mut s = escena_del_prendedor();
2422        let mano = [(Vec3::new(30.0, 10.0, 30.0), 0.4, Vec3::new(0.3, 0.9, 0.2))];
2423        assert!(s.luces_en(2.0, &mano).is_empty(), "de día la estrella no alumbra");
2424        s.cielo = Some(crate::Cielo::noche());
2425        assert!(s.luces_en(0.5, &mano).is_empty(), "antes de aparecer no alumbra");
2426        let luces = s.luces_en(2.0, &mano);
2427        assert_eq!(luces.len(), 1, "sólo la estrella alumbra, no la flor ni la perla");
2428        let estrella = &s.utileria_en(2.0, &mano)[0];
2429        let (c, r) = estrella.bound();
2430        let centro = Vec3::from_array(estrella.pos) + crate::sdf_kit::rot_y(Vec3::from_array(c), estrella.yaw);
2431        assert!(Vec3::from_array(luces[0].pos).distance(centro) < 1e-4, "la luz sale de la estrella");
2432        assert!(luces[0].range > r * 4.0, "alumbra más allá de sí misma: {}", luces[0].range);
2433        let creciendo = s.luces_en(1.0 + crate::APARECER * 0.5, &mano)[0].range;
2434        assert!(creciendo < luces[0].range, "la luz crece con la estrella");
2435        // Luz propia: la estrella cruza el umbral del shader; las demás piezas no.
2436        let piezas = s.utileria_en(2.0, &mano);
2437        let brilla = |x: &llimphi_3d::SdfSer| x.prims.iter().any(|p| p.color.iter().any(|&k| k > 1.5));
2438        assert!(brilla(&piezas[0]), "la estrella no tiene luz propia");
2439        assert!(piezas[1..].iter().all(|x| !brilla(x)), "una pieza sin luz propia brilla");
2440        // Viaja en el documento, en los dos formatos.
2441        let vuelta: SceneSpec = ron::from_str(&ron::to_string(&s).unwrap()).unwrap();
2442        assert_eq!(vuelta.cielo, s.cielo);
2443        let de_postcard: SceneSpec = postcard::from_bytes(&postcard::to_allocvec(&s).unwrap()).unwrap();
2444        assert_eq!(de_postcard.cielo, s.cielo);
2445        let vieja: SceneSpec = ron::from_str(r#"(name: "a", mundo: 0, duration: 4.0, actors: [])"#).unwrap();
2446        assert!(vieja.cielo.is_none(), "una escena vieja abre de día");
2447    }
2448
2449    /// El decorado viaja en el documento (RON y postcard), y una escena vieja
2450    /// abre sin decorado. Y el arranque trae el mar y la luna como mundos.
2451    #[test]
2452    fn el_decorado_viaja_en_el_documento_y_el_arranque_trae_mar_y_luna() {
2453        let mut s = SceneSpec::walk_and_emote("palacio", 0, 1, Clip::Idle, [96, 24, 96]);
2454        s.decorado.push(crate::Decorado::en(crate::CasaTipo::Palacio, 48.0, 70.0));
2455        s.decorado.push(crate::Decorado::en(crate::CasaTipo::Kiosko, 30.0, 60.0));
2456        let vuelta: SceneSpec = ron::from_str(&ron::to_string(&s).unwrap()).unwrap();
2457        assert_eq!(vuelta.decorado, s.decorado);
2458        let de_postcard: SceneSpec = postcard::from_bytes(&postcard::to_allocvec(&s).unwrap()).unwrap();
2459        assert_eq!(de_postcard.decorado, s.decorado);
2460        let vieja: SceneSpec = ron::from_str(r#"(name: "a", mundo: 0, duration: 4.0, actors: [])"#).unwrap();
2461        assert!(vieja.decorado.is_empty());
2462        let p = Project::starter();
2463        for nombre in ["mar", "luna"] {
2464            let m = p.mundos.iter().find(|m| m.name == nombre).unwrap_or_else(|| panic!("falta el mundo {nombre}"));
2465            assert!(p.render_mundo(m.id).is_some(), "{nombre} no se puede pintar");
2466        }
2467    }
2468
2469    #[test]
2470    fn la_especie_viaja_en_el_documento_y_uno_viejo_abre_persona() {
2471        let viejo = r#"(name: "a", age: Adult, skin: (0.9, 0.7, 0.6),
2472            shirt: (0.8, 0.3, 0.3), pants: (0.2, 0.2, 0.3))"#;
2473        let v: CharSpec = ron::from_str(viejo).unwrap();
2474        assert_eq!(v.bicho, None);
2475        let e = elefante();
2476        let vuelta: CharSpec = ron::from_str(&ron::to_string(&e).unwrap()).unwrap();
2477        assert_eq!(vuelta.bicho, Some(Bicho::Elefante));
2478        let bytes = postcard::to_allocvec(&e).unwrap();
2479        let de_postcard: CharSpec = postcard::from_bytes(&bytes).unwrap();
2480        assert_eq!(de_postcard.bicho, Some(Bicho::Elefante));
2481    }
2482
2483    #[test]
2484    fn ciclar_la_especie_recorre_el_bestiario_y_vuelve_a_ninguna() {
2485        let mut c = CharSpec::new("x", Age::Adult);
2486        let mut vistos = 0;
2487        loop {
2488            c.cycle_bicho(false);
2489            if c.bicho.is_none() {
2490                break;
2491            }
2492            vistos += 1;
2493        }
2494        assert_eq!(vistos, Bicho::ALL.len());
2495        c.cycle_bicho(true);
2496        assert_eq!(c.bicho, Some(Bicho::ALL[Bicho::ALL.len() - 1]));
2497    }
2498
2499    #[test]
2500    fn el_reparto_entra_en_el_presupuesto_del_uniform() {
2501        use llimphi_3d::{VOXEL_MAX_SERS, VOXEL_MAX_SER_PRIMS};
2502        // 16 humanoides con la cara entera son ~500 primitivas: NO entran. El reparto
2503        // tiene que hacerlos entrar bajando detalle, no dejándolos afuera.
2504        let specs = bandada(16);
2505        let sers = reparto_sdf(&en_cuadro(&specs, 6.0), Vec3::new(0.0, 1.6, -4.0), true);
2506        let total: usize = sers.iter().map(|s| s.prims.len()).sum();
2507        assert!(sers.len() <= VOXEL_MAX_SERS);
2508        assert!(total <= VOXEL_MAX_SER_PRIMS, "{total} primitivas");
2509        assert_eq!(sers.len(), 16, "los 16 entran (con LOD)");
2510        // Nadie sale mutilado: todos tienen al menos el cuerpo completo.
2511        for s in &sers {
2512            assert!(s.prims.len() >= 10, "cuerpo incompleto: {}", s.prims.len());
2513        }
2514    }
2515
2516    #[test]
2517    fn los_de_cerca_llevan_mas_cara_que_los_del_fondo() {
2518        let specs = bandada(8);
2519        let sers = reparto_sdf(&en_cuadro(&specs, 12.0), Vec3::new(0.0, 1.6, -2.0), true);
2520        // Vienen ordenados de cerca a lejos, y el detalle decae monótonamente.
2521        assert!(
2522            sers[0].prims.len() > sers[sers.len() - 1].prims.len(),
2523            "{} vs {}",
2524            sers[0].prims.len(),
2525            sers[sers.len() - 1].prims.len()
2526        );
2527        for par in sers.windows(2) {
2528            assert!(par[0].prims.len() >= par[1].prims.len(), "el detalle no puede subir con la distancia");
2529        }
2530    }
2531
2532    #[test]
2533    fn una_multitud_no_pierde_a_nadie_el_fondo_pasa_a_malla() {
2534        use llimphi_3d::{VOXEL_MAX_SERS, VOXEL_MAX_SER_PRIMS};
2535        // Más humanoides que los que admite el uniform. Antes, los del fondo
2536        // desaparecían del cuadro; ahora bajan a malla de cajas.
2537        //
2538        // El número sale del TOPE, no de una constante escrita a mano: cuando el
2539        // uniform pasó de 16 seres a 40 (2026-09-06, la multitud de la vereda) este
2540        // test se puso rojo con 30 porque ya entraban todos — y lo que quería fijar
2541        // no era «30 no entran» sino «el que no entra no se pierde».
2542        let n = VOXEL_MAX_SERS * 2;
2543        let specs = bandada(n);
2544        let cuadro = en_cuadro(&specs, 4.0);
2545        let ojo = Vec3::new(0.0, 1.6, -4.0);
2546        let half = Vec3::new(64.0, 32.0, 64.0);
2547        let rep = reparto_mixto(&cuadro, ojo, true, half);
2548        assert_eq!(rep.sers.len() + rep.mallas.len(), n, "nadie se cae del cuadro");
2549        assert!(rep.sers.len() <= VOXEL_MAX_SERS);
2550        let total: usize = rep.sers.iter().map(|s| s.prims.len()).sum();
2551        assert!(total <= VOXEL_MAX_SER_PRIMS, "{total} primitivas");
2552        assert!(!rep.mallas.is_empty(), "con {n}, alguno cae a malla");
2553        // Las mallas traen geometría de verdad (no cascarones vacíos).
2554        for (_, v, i) in &rep.mallas {
2555            assert!(!v.is_empty() && !i.is_empty());
2556        }
2557    }
2558
2559    #[test]
2560    fn el_sdf_se_lo_llevan_los_de_cerca() {
2561        let specs = bandada(24);
2562        let cuadro = en_cuadro(&specs, 5.0);
2563        let ojo = Vec3::new(0.0, 1.6, -4.0);
2564        let half = Vec3::ZERO;
2565        let rep = reparto_mixto(&cuadro, ojo, true, half);
2566        // El más lejano de los SDF está más cerca que el más cercano de las mallas
2567        // (la frontera entre técnicas es la distancia, no el orden de la lista).
2568        let z_sdf = rep.sers.iter().map(|s| s.pos[2]).fold(f32::MIN, f32::max);
2569        let z_malla = rep
2570            .mallas
2571            .iter()
2572            .map(|(m, _, _)| m.to_cols_array()[14])
2573            .fold(f32::MAX, f32::min);
2574        assert!(z_sdf < z_malla, "SDF hasta z={z_sdf}, malla desde z={z_malla}");
2575    }
2576
2577    #[test]
2578    fn el_reparto_ordena_por_cercania_no_por_indice() {
2579        let specs = bandada(3);
2580        let mut cuadro = en_cuadro(&specs, 20.0);
2581        // El ÚLTIMO de la lista es el que está pegado al ojo.
2582        cuadro[2].pos = Vec3::new(0.0, 0.0, -1.0);
2583        let ojo = Vec3::new(0.0, 1.6, -3.0);
2584        let sers = reparto_sdf(&cuadro, ojo, true);
2585        assert!((sers[0].pos[2] - (-1.0)).abs() < 1e-3, "el más cercano va primero");
2586    }
2587
2588    #[test]
2589    fn ley_ecuacion_compila_y_corre() {
2590        use crate::ecuacion::FieldEngine;
2591        // Autoría de una ley Gray–Scott con los tipos del studio (como haría la UI).
2592        let ley = LeyKind::Ecuacion {
2593            campos: vec![
2594                FieldDef::new("u", 1.0, 0.0, 1.0),
2595                FieldDef::new("v", 0.0, 0.0, 1.0),
2596            ],
2597            params: vec![
2598                ParamDef::new("Du", 0.16, 0.0, 1.0),
2599                ParamDef::new("Dv", 0.08, 0.0, 1.0),
2600                ParamDef::new("F", 0.06, 0.0, 0.1),
2601                ParamDef::new("k", 0.062, 0.0, 0.1),
2602            ],
2603            fuentes: vec![
2604                "Du * lap(u) - u * v * v + F * (1 - u)".into(),
2605                "Dv * lap(v) + u * v * v - (F + k) * v".into(),
2606            ],
2607        };
2608        let prog = ley.compile_ecuacion().expect("es Ecuacion").expect("compila");
2609        let LeyKind::Ecuacion { campos, .. } = &ley else { unreachable!() };
2610        let mut eng = FieldEngine::new([32, 1, 32], campos.clone());
2611        for z in 14..18 {
2612            for x in 14..18 {
2613                eng.set(1, x, 0, z, 0.5);
2614                eng.set(0, x, 0, z, 0.25);
2615            }
2616        }
2617        let params = ley.ecuacion_param_values();
2618        for _ in 0..300 {
2619            eng.step(&prog, &params, 1.0);
2620        }
2621        let (mut lo, mut hi) = (f32::INFINITY, f32::NEG_INFINITY);
2622        for z in 0..32 {
2623            for x in 0..32 {
2624                let v = eng.get(1, x, 0, z);
2625                lo = lo.min(v);
2626                hi = hi.max(v);
2627            }
2628        }
2629        assert!(hi - lo > 0.05, "la ley autorada formó patrón (rango={})", hi - lo);
2630    }
2631
2632    #[test]
2633    fn ley_ecuacion_error_de_parseo_ubicado() {
2634        let ley = LeyKind::Ecuacion {
2635            campos: vec![FieldDef::new("c", 0.0, 0.0, 1.0)],
2636            params: vec![],
2637            fuentes: vec!["k * lap(c)".into()], // `k` no declarado
2638        };
2639        let err = ley.compile_ecuacion().unwrap().unwrap_err();
2640        assert!(err.contains("campo «c»"), "ubica el campo: {err}");
2641        assert!(err.contains('k'), "menciona el símbolo desconocido: {err}");
2642    }
2643
2644    #[test]
2645    fn proyecto_round_trip_ron() {
2646        let p = Project::starter();
2647        let s = ron::ser::to_string(&p).expect("serializa a ron");
2648        let back: Project = ron::from_str(&s).expect("deserializa de ron");
2649        assert_eq!(back.biomas.len(), p.biomas.len());
2650        assert_eq!(back.mundos.len(), p.mundos.len());
2651        assert_eq!(back.biomas[0].name, "desierto");
2652        // El relieve del bioma sobrevive el viaje (un parámetro de muestra).
2653        assert!((back.biomas[0].base - p.biomas[0].base).abs() < 1e-6);
2654        // Y los ids siguen apuntando: la escena referencia un mundo que existe.
2655        assert!(back.mundo(back.escenas[0].mundo).is_some());
2656    }
2657
2658    #[test]
2659    fn mundo_desde_mapa_persiste_en_ron() {
2660        let mut p = Project::starter();
2661        // Adjuntá una fuente de mapa a un mundo (como al importar en el studio).
2662        let mid = p.mundos[0].id;
2663        p.mundos[0].mapa = Some(MapaFuente {
2664            geojson: r#"{"type":"FeatureCollection","features":[]}"#.into(),
2665            pgm: Some(vec![1, 2, 3, 4]),
2666            dem_tif: None,
2667            rango_dem_m: 250.0,
2668        });
2669        let s = ron::ser::to_string(&p).expect("serializa a ron");
2670        let back: Project = ron::from_str(&s).expect("deserializa de ron");
2671        let m = back.mundo(mid).expect("el mundo sigue ahí");
2672        let fuente = m.mapa.as_ref().expect("la fuente del mapa sobrevive el viaje");
2673        assert!(fuente.geojson.contains("FeatureCollection"));
2674        assert_eq!(fuente.pgm.as_deref(), Some(&[1, 2, 3, 4][..]));
2675        assert!((fuente.rango_dem_m - 250.0).abs() < 1e-6);
2676    }
2677
2678    #[test]
2679    fn ron_viejo_sin_mapa_carga_como_procedural() {
2680        // Un mundo serializado ANTES de que existiera el campo `mapa` debe abrir
2681        // con `mapa: None` (compat hacia atrás vía `#[serde(default)]`).
2682        let ron_viejo = r#"(id:7,name:"viejo",seed:1,biomas:[])"#;
2683        let m: Mundo = ron::from_str(ron_viejo).expect("mundo legacy deserializa");
2684        assert!(m.mapa.is_none(), "sin campo mapa = procedural");
2685    }
2686
2687    #[test]
2688    fn herencia_de_material_overridea_solo_lo_redefinido() {
2689        let mut p = Project::starter();
2690        // «cactus amarillo» hijo de «cactus»: sólo redefine el color.
2691        let cactus = p.material_id_for(Material::Cactus).unwrap();
2692        let verde = p.resolve_material(cactus);
2693        let hijo_id = p.alloc_id();
2694        p.materiales.push(MaterialDef {
2695            id: hijo_id,
2696            name: "cactus amarillo".into(),
2697            parent: Some(cactus),
2698            role: MatRole::Objeto(Forma::Columnar),
2699            color: Some([0.9, 0.85, 0.2]),
2700            grain: None, // hereda
2701            brillo: None,
2702            rugosidad: None,
2703            bisel: None,
2704            leyes: vec![],
2705            builtin: None,
2706        });
2707        let hijo = p.resolve_material(hijo_id);
2708        assert_ne!(hijo.color, verde.color, "el color se redefinió");
2709        assert_eq!(hijo.grain, verde.grain, "el grano se heredó del padre");
2710    }
2711
2712    #[test]
2713    fn agua_fluye_por_su_ley_y_hereda_leyes() {
2714        let p = Project::starter();
2715        // El agua semilla tiene la ley Fluir con sus params.
2716        let (g, h) = p.water_fluir().expect("el agua tiene ley Fluir");
2717        assert!(g > 0.0 && h >= 0.0);
2718        // Un material sin leyes no fluye.
2719        let roca = p.material_id_for(Material::Rock).unwrap();
2720        assert!(p.fluir_params(roca).is_none());
2721        // El cactus crece (ley Crecer en el starter).
2722        let cactus = p.material_id_for(Material::Cactus).unwrap();
2723        assert!(p.crecer_velocidad(cactus).is_some());
2724
2725        // Herencia de leyes: un hijo del agua hereda Fluir aunque no la redefina.
2726        let mut p2 = p.clone();
2727        let agua = p2.material_id_for(Material::Water).unwrap();
2728        let hijo = p2.alloc_id();
2729        p2.materiales.push(MaterialDef {
2730            id: hijo,
2731            name: "agua turbia".into(),
2732            parent: Some(agua),
2733            role: MatRole::Terreno,
2734            color: Some([0.2, 0.4, 0.5]),
2735            grain: None,
2736            brillo: None,
2737            rugosidad: None,
2738            bisel: None,
2739            leyes: vec![],
2740            builtin: None,
2741        });
2742        assert!(p2.fluir_params(hijo).is_some(), "el hijo hereda Fluir del padre");
2743    }
2744
2745    #[test]
2746    fn detalle_sub_voxel_hereda_y_overridea() {
2747        let mut p = Project::starter();
2748        // Default = liso/duro (0,0) para un material sin detalle declarado.
2749        let roca = p.material_id_for(Material::Rock).unwrap();
2750        assert_eq!(p.detalle_material(roca), (0.0, 0.0), "default = sin detalle");
2751        // Padre con rugosidad; hijo sin nada → la hereda. Nieto redefine bisel.
2752        let padre = p.alloc_id();
2753        p.materiales.push(MaterialDef {
2754            id: padre,
2755            name: "roca rugosa".into(),
2756            parent: None,
2757            role: MatRole::Terreno,
2758            color: None,
2759            grain: None,
2760            brillo: None,
2761            rugosidad: Some(0.9),
2762            bisel: None,
2763            leyes: vec![],
2764            builtin: None,
2765        });
2766        let hijo = p.alloc_id();
2767        p.materiales.push(MaterialDef {
2768            id: hijo,
2769            name: "hijo".into(),
2770            parent: Some(padre),
2771            role: MatRole::Terreno,
2772            color: None,
2773            grain: None,
2774            brillo: None,
2775            rugosidad: None,
2776            bisel: Some(0.7),
2777            leyes: vec![],
2778            builtin: None,
2779        });
2780        let (rug, bis) = p.detalle_material(hijo);
2781        assert!((rug - 0.9).abs() < 1e-6, "rugosidad heredada del padre");
2782        assert!((bis - 0.7).abs() < 1e-6, "bisel propio gana");
2783    }
2784
2785    #[test]
2786    fn detalle_por_voxel_se_hornea_por_material() {
2787        // Cierra la limitación HONESTA: dos materiales del bioma deben lucir distinto a
2788        // la vez → cada uno hornea su detalle POR VOXEL en el grid, vía el pipeline real
2789        // (resolve_material → bioma_palette → generate_window → grid.detail_at).
2790        let mut p = Project::starter();
2791        let b0 = p.biomas[0].clone();
2792        let (g_id, c_id) = (b0.ground, b0.cliff);
2793        assert_ne!(g_id, c_id, "el bioma starter distingue suelo de acantilado");
2794
2795        // Suelo RUGOSO+duro (rug alto, bisel 0); acantilado LISO (rug 0, bisel 1).
2796        let set_det = |p: &mut Project, id: u64, r: f32, b: f32| {
2797            let m = p.materiales.iter_mut().find(|m| m.id == id).unwrap();
2798            m.rugosidad = Some(r);
2799            m.bisel = Some(b);
2800        };
2801        set_det(&mut p, g_id, 1.4, 0.0);
2802        set_det(&mut p, c_id, 0.0, 1.0);
2803
2804        // La paleta resuelta lleva el detalle POR MATERIAL (lo que el escalar global
2805        // no distinguía).
2806        let rg = p.resolve_material(g_id);
2807        let rc = p.resolve_material(c_id);
2808        assert_eq!(rg.detalle, (1.4, 0.0), "suelo rugoso/duro");
2809        assert_eq!(rc.detalle, (0.0, 1.0), "acantilado liso/biselado");
2810        assert_ne!(rg.detalle, rc.detalle, "dos materiales, dos looks");
2811
2812        // Y se hornea por voxel al generar la ventana: aparecen ambos códigos distintos
2813        // + aire sin autor (retrocompat). Byte de detalle = 1 + round(norm·254): el
2814        // suelo da rug alto (~238) y bisel piso (1); el acantilado, rug piso y bisel alto.
2815        let palette = p.bioma_palette(&b0);
2816        let grid = b0.generate_window(7, &palette, [40, 48, 40], [0, 0]);
2817        let (mut suelo, mut cliff, mut aire) = (false, false, false);
2818        let dim = grid.dim();
2819        for z in 0..dim[2] {
2820            for y in 0..dim[1] {
2821                for x in 0..dim[0] {
2822                    match grid.detail_at(x, y, z).unwrap() {
2823                        [r, b] if r >= 200 && b <= 5 => suelo = true,
2824                        [r, b] if r <= 5 && b >= 200 => cliff = true,
2825                        [0, 0] => aire = true,
2826                        _ => {}
2827                    }
2828                }
2829            }
2830        }
2831        assert!(suelo, "el suelo rugoso se horneó por voxel");
2832        assert!(cliff, "el acantilado liso/biselado se horneó por voxel (distinto del suelo)");
2833        assert!(aire, "quedan voxels sin autor (aire/agua) en look global — retrocompat");
2834    }
2835
2836    #[test]
2837    fn herencia_con_ciclo_no_cuelga() {
2838        let mut p = Project::starter();
2839        let a = p.alloc_id();
2840        let b = p.alloc_id();
2841        p.materiales.push(MaterialDef { id: a, name: "a".into(), parent: Some(b), role: MatRole::Terreno, color: None, grain: None, brillo: None, rugosidad: None, bisel: None, leyes: vec![], builtin: None });
2842        p.materiales.push(MaterialDef { id: b, name: "b".into(), parent: Some(a), role: MatRole::Terreno, color: None, grain: None, brillo: None, rugosidad: None, bisel: None, leyes: vec![], builtin: None });
2843        // No debe colgarse; cae al gris neutro.
2844        let r = p.resolve_material(a);
2845        assert_eq!(r.color, [150, 150, 150]);
2846    }
2847
2848    #[test]
2849    fn charspec_se_materializa_con_la_edad() {
2850        let spec = CharSpec::new("nene", Age::Baby);
2851        let actor = spec.to_actor(Vec3::ZERO, 0.0);
2852        assert_eq!(actor.age, Age::Baby);
2853    }
2854
2855    #[test]
2856    fn world_dim_minimo_48_de_alto() {
2857        assert_eq!(world_dim(64)[1], 48); // 64*0.4=25.6 → clamp a 48
2858        assert_eq!(world_dim(192)[1], 76);
2859    }
2860
2861    #[test]
2862    fn escena_round_trip_y_compila_a_guiones() {
2863        let dim = world_dim(128);
2864        let s = SceneSpec::walk_and_emote("demo", 0, 3, Clip::Wave, dim);
2865        // RON ida y vuelta.
2866        let txt = ron::ser::to_string(&s).expect("ron");
2867        let back: SceneSpec = ron::from_str(&txt).expect("de-ron");
2868        assert_eq!(back.actors.len(), 3);
2869        // Compila a guiones reproducibles: a mitad de la caminata el actor se movió.
2870        let scripts = back.scripts();
2871        let start = scripts[0].sample(0.0);
2872        let mid = scripts[0].sample(1.3);
2873        assert!(mid.gx > start.gx, "el actor avanza en X: {} → {}", start.gx, mid.gx);
2874    }
2875
2876    #[test]
2877    fn plano_vigente_corta_en_el_tiempo() {
2878        let dim = world_dim(128);
2879        let s = SceneSpec::walk_and_emote("demo", 0, 2, Clip::Wave, dim);
2880        // Arranca en establecedor; tras el giro (t≈3) pasa a primer plano.
2881        assert_eq!(s.active_shot(0.5), ShotKind::Establishing);
2882        assert_eq!(s.active_shot(3.5), ShotKind::CloseUp);
2883        // El plano resuelve una cámara que mira al centroide.
2884        let look = Vec3::new(10.0, 2.0, 10.0);
2885        let cam = ShotKind::CloseUp.resolve(look, 9.0, 1.0);
2886        assert_eq!(cam.target, look);
2887        assert!((cam.eye - look).length() > 1.0, "el ojo está separado del objetivo");
2888    }
2889
2890    #[test]
2891    fn beats_son_cortes_y_gestos() {
2892        let dim = world_dim(128);
2893        let s = SceneSpec::walk_and_emote("demo", 0, 2, Clip::Wave, dim);
2894        // walk_and_emote: corte de cámara a t_turn (3.0) + el gesto Wave a t_turn.
2895        // Caen en el mismo instante → se funden en un solo beat.
2896        let beats = s.beat_times();
2897        assert!(!beats.is_empty(), "hay al menos un acento");
2898        assert!(beats.iter().all(|&t| t >= 0.0 && t <= s.duration + 0.1));
2899        assert!(beats.iter().any(|&t| (t - 3.0).abs() < 0.2), "acento cerca del gesto/corte");
2900    }
2901
2902    #[test]
2903    fn camara_en_mano_es_determinista_y_apagable() {
2904        // amt=0 → trípode: sin offset, exactamente cero.
2905        let (e0, t0) = handheld_shake(1.234, 0.0, 30.0);
2906        assert_eq!(e0, Vec3::ZERO);
2907        assert_eq!(t0, Vec3::ZERO);
2908
2909        // amt>0 → tiembla (offset no nulo) y es función pura de t (reproducible).
2910        let (e1, _) = handheld_shake(1.234, 0.7, 30.0);
2911        let (e2, _) = handheld_shake(1.234, 0.7, 30.0);
2912        assert_eq!(e1, e2, "mismo t → mismo temblor (peli reproducible)");
2913        assert!(e1.length() > 0.0, "con intensidad la cámara se mueve");
2914        // Instantes distintos → temblor distinto (no está congelado).
2915        let (e3, _) = handheld_shake(1.235, 0.7, 30.0);
2916        assert!((e1 - e3).length() > 0.0, "el temblor evoluciona en el tiempo");
2917    }
2918
2919    #[test]
2920    fn frame_rate_del_heroe_viaja_al_guion() {
2921        let dim = world_dim(128);
2922        let s = SceneSpec::walk_and_emote("demo", 0, 3, Clip::Wave, dim);
2923        // El primer actor (Héroe) anima en doses; los demás, fluidos.
2924        assert_eq!(s.actors[0].frame_rate, Some(12));
2925        assert_eq!(s.actors[1].frame_rate, None);
2926        // Y sobrevive la compilación a guion.
2927        assert_eq!(s.scripts()[0].frame_rate(), Some(12));
2928        // Cámara en mano por defecto encendida.
2929        assert!(s.handheld > 0.0);
2930    }
2931
2932    #[test]
2933    fn ventana_de_escena_sigue_al_reparto() {
2934        let dim = world_dim(128); // [128, 51, 128] → medio = 64
2935        // Un actor que camina lejos del origen (más allá de la caja finita vieja):
2936        // un keyframe en grilla 18, otro en 600 → coords de MUNDO, no acotadas.
2937        let s = SceneSpec {
2938            distancia: None,
2939            id: 0,
2940            name: "caminata larga".into(),
2941            mundo: 0,
2942            duration: 10.0,
2943            actors: vec![ActorSpec {
2944                character: 0,
2945                keys: vec![
2946                    ActorKeySpec { t: 0.0, gx: 18.0, gz: 64.0, clip: None, face: None },
2947                    ActorKeySpec { t: 10.0, gx: 600.0, gz: 64.0, clip: None, face: None },
2948                ],
2949                frame_rate: None,
2950            }],
2951            shots: vec![],
2952            handheld: 0.0,
2953            atmosfera: None,
2954            caida: None,
2955            sucesos: Vec::new(),
2956            utileria: Vec::new(),
2957            cielo: None,
2958            decorado: Vec::new(),
2959            jardin: None,
2960        };
2961        let scripts = s.scripts();
2962
2963        // Al final el reparto está cerca de wx=600: la ventana lo centra (su columna
2964        // local cae cerca de dim/2), cosa imposible en una caja fija centrada en 0.
2965        let o_fin = window_origin_for_cast(&scripts, 10.0, dim);
2966        let local_x = 600 - o_fin[0];
2967        assert!(
2968            (local_x - dim[0] as i32 / 2).abs() <= SCENE_WINDOW_STEP,
2969            "el reparto queda centrado en la ventana (local_x={local_x})"
2970        );
2971
2972        // El origen se snappea al paso (sólo salta a múltiplos) → no regenera por cuadro.
2973        assert_eq!(o_fin[0] % SCENE_WINDOW_STEP, 0);
2974        assert_eq!(o_fin[1] % SCENE_WINDOW_STEP, 0);
2975
2976        // Y la ventana se MUEVE entre el principio y el final (mundo infinito, no caja).
2977        let o_ini = window_origin_for_cast(&scripts, 0.0, dim);
2978        assert_ne!(o_ini[0], o_fin[0], "la ventana siguió al reparto en X");
2979
2980        // Sin reparto: mundo centrado en el origen (compatibilidad con el preview fijo).
2981        assert_eq!(window_origin_for_cast(&[], 0.0, dim), [0, 0]);
2982    }
2983
2984    #[test]
2985    fn starter_trae_escena_y_personajes() {
2986        let p = Project::starter();
2987        assert_eq!(p.seres.len(), 3);
2988        assert_eq!(p.escenas.len(), 1);
2989        // La escena referencia un mundo válido por id.
2990        assert!(p.mundo(p.escenas[0].mundo).is_some());
2991        // Los 30 materiales semilla están sembrados (flora + terreno + casa).
2992        assert_eq!(p.materiales.len(), 31);
2993        assert!(p.material_id_for(Material::Sand).is_some());
2994        assert!(p.material_id_for(Material::Basalto).is_some());
2995        assert!(p.material_id_for(Material::Ladrillo).is_some());
2996        assert!(p.material_id_for(Material::Vidrio).is_some());
2997        // El vidrio (y las rocas pulidas) resuelven brillo > 0; el ladrillo, mate.
2998        assert!(p.resolve_material(p.material_id_for(Material::Vidrio).unwrap()).brillo > 0.5);
2999        assert_eq!(p.resolve_material(p.material_id_for(Material::Ladrillo).unwrap()).brillo, 0.0);
3000    }
3001}