tawasuyu

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Definiciones

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 llimphi-voxel/src/studio.rs Rust · 2001 líneas · 83.2 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! `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::conducta::Conducta;
18use crate::ecuacion::{Assign, Expr, FieldDef, Program, Symbols};
19use crate::casa::{CasaTipo, CasaUso};
20use crate::mineral::{Mineral, MineralUso};
21use crate::rig::{Andar, Movimiento, Rig};
22use crate::worldgen::{Bioma, BiomaPalette, Forma, Material, ResolvedMaterial};
23use llimphi_3d::glam::{Mat4, Vec3};
24use llimphi_3d::{Camera3d, Vertex3d};
25
26/// Dimensión por defecto de la grilla con la que el editor previsualiza un mundo
27/// (cúbica en XZ, alto = 0.4·lado, mínimo 48) — el mismo criterio que la app.
28pub const PREVIEW_DIM_XZ: u32 = 128;
29
30/// **Dirección del sol** de la escena (hacia el sol, sin normalizar). Una sola
31/// fuente de verdad: la usa el preview/export para iluminar (`voxel.sun_dir`) y el
32/// plano [`ShotKind::Backlight`] para saber adónde mirar. Sol algo bajo → luz
33/// rasante (claroscuro) y, en contraluz, **god rays** legibles.
34pub const SCENE_SUN: [f32; 3] = [0.5, 0.36, 0.45];
35
36/// Calcula el `dim` `[x, y, z]` de un mundo de lado `xz` (alto derivado).
37pub fn world_dim(xz: u32) -> [u32; 3] {
38    let dy = (xz * 4 / 10).max(48);
39    [xz, dy, xz]
40}
41
42/// **Paso de reubicación** de la ventana de una escena (voxels). El origen sólo
43/// salta a múltiplos de este valor: así la ventana no se regenera cada cuadro al
44/// caminar el reparto, sólo al cruzar un paso. Como la cámara mira al centroide del
45/// reparto (en espacio de ventana), el salto traslada terreno + cámara + actores
46/// **juntos** → sin pop visible (la imagen es continua), pero el mundo no tiene
47/// borde: el reparto puede caminar indefinidamente. Ver [`window_origin_for_cast`].
48pub const SCENE_WINDOW_STEP: i32 = 16;
49
50/// Redondea `v` al múltiplo de `step` hacia abajo (floor con signo) — espeja el
51/// `snap` del [`WorldStream`](crate::WorldStream).
52#[inline]
53fn snap(v: i32, step: i32) -> i32 {
54    v.div_euclid(step) * step
55}
56
57/// **Origen de ventana (esquina, coords de mundo) que centra al reparto** de una
58/// escena en el instante `t`: promedia las columnas `(gx, gz)` de los guiones
59/// (tomadas en su tiempo cuantizado, igual que al posar) y resta medio `dim`,
60/// snappeado a [`SCENE_WINDOW_STEP`]. Es lo que hace que **toda escena viva en un
61/// mundo infinito**: la ventana voxel sigue al reparto en vez de ser una caja fija
62/// centrada en el origen. Sin reparto, devuelve `[0, 0]` (mundo centrado, como
63/// antes).
64pub fn window_origin_for_cast(scripts: &[ActorScript], t: f32, dim: [u32; 3]) -> [i32; 2] {
65    if scripts.is_empty() {
66        return [0, 0];
67    }
68    let (mut cwx, mut cwz) = (0.0_f32, 0.0_f32);
69    for s in scripts {
70        let smp = s.sample(s.quantize(t));
71        cwx += smp.gx;
72        cwz += smp.gz;
73    }
74    let n = scripts.len() as f32;
75    let (cwx, cwz) = (cwx / n, cwz / n);
76    [
77        snap(cwx as i32 - dim[0] as i32 / 2, SCENE_WINDOW_STEP),
78        snap(cwz as i32 - dim[2] as i32 / 2, SCENE_WINDOW_STEP),
79    ]
80}
81
82// =============================================================================
83//  Nivel 1 — Leyes (físicas / comportamientos)
84// =============================================================================
85
86/// Un **parámetro autorado** de una ley [`Ecuacion`](LeyKind::Ecuacion): nombre (que
87/// la fórmula referencia), valor y rango (para el slider). Es el equivalente dinámico
88/// de los `(nombre, valor, min, max)` cableados de `Fluir`/`Crecer`.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct ParamDef {
91    pub name: String,
92    pub value: f32,
93    pub min: f32,
94    pub max: f32,
95}
96
97impl ParamDef {
98    pub fn new(name: impl Into<String>, value: f32, min: f32, max: f32) -> Self {
99        Self { name: name.into(), value, min, max }
100    }
101}
102
103/// **Tipo de ley** (comportamiento matemático). Enum extensible: `Fluir` (líquidos,
104/// autómata nativo que mueve celdas) y `Crecer` (flora) son leyes **nativas** con
105/// runtime cableado; `Ecuacion` es la ley **autorable** — un sistema de ecuaciones de
106/// campo escrito por el usuario y compilado (ver [`crate::ecuacion`]). Cada variante
107/// nativa expone sus parámetros editables vía [`params`](Self::params) para que la UI
108/// arme sliders; la `Ecuacion` declara sus propios campos y parámetros.
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub enum LeyKind {
111    /// Líquido: se esparce a vecinos en horizontal y cae con gravedad (cascadas).
112    Fluir { gravedad: f32, horizontal: f32 },
113    /// Crece a una velocidad VARIABLE (flora).
114    Crecer { velocidad: f32 },
115    /// **Ley autorable**: campos de estado + parámetros + una ecuación
116    /// `Δcampoₖ/dt = fuenteₖ` por campo (misma cantidad y orden que `campos`). El
117    /// texto `fuentes[k]` se parsea con los nombres de `campos`/`params`. Generaliza
118    /// difusión, reacción‑difusión, calor, crecimiento… con la misma máquina.
119    Ecuacion {
120        campos: Vec<FieldDef>,
121        params: Vec<ParamDef>,
122        fuentes: Vec<String>,
123    },
124}
125
126impl LeyKind {
127    /// Un ejemplar de cada tipo, con valores por defecto (para crear/ciclar).
128    pub fn all_defaults() -> Vec<LeyKind> {
129        vec![
130            LeyKind::Fluir { gravedad: 1.0, horizontal: 0.6 },
131            LeyKind::Crecer { velocidad: 1.0 },
132            LeyKind::ecuacion_default(),
133        ]
134    }
135
136    /// Ley autorable por defecto: **difusión** de un campo `c` (`Δc/dt = k·lap(c)`).
137    /// El punto de partida más simple que ya *hace algo* visible en el laboratorio.
138    pub fn ecuacion_default() -> LeyKind {
139        LeyKind::Ecuacion {
140            campos: vec![FieldDef::new("c", 0.0, 0.0, 1.0)],
141            params: vec![ParamDef::new("k", 0.15, 0.0, 0.25)],
142            fuentes: vec!["k * lap(c)".to_string()],
143        }
144    }
145
146    /// Nombre legible (español).
147    pub fn label(&self) -> &'static str {
148        match self {
149            LeyKind::Fluir { .. } => "fluir",
150            LeyKind::Crecer { .. } => "crecer",
151            LeyKind::Ecuacion { .. } => "ecuación",
152        }
153    }
154
155    /// El tipo siguiente (cicla por los defaults), preservando nada (resetea params).
156    pub fn next(&self) -> LeyKind {
157        let all = Self::all_defaults();
158        let i = all.iter().position(|k| k.label() == self.label()).unwrap_or(0);
159        all[(i + 1) % all.len()].clone()
160    }
161
162    /// Parámetros editables: `(nombre, valor, min, max)`. La UI arma un slider por cada.
163    pub fn params(&self) -> Vec<(&'static str, f32, f32, f32)> {
164        match self {
165            LeyKind::Fluir { gravedad, horizontal } => vec![
166                ("gravedad", *gravedad, 0.0, 1.0),
167                ("horizontal", *horizontal, 0.0, 1.0),
168            ],
169            LeyKind::Crecer { velocidad } => vec![("velocidad", *velocidad, 0.0, 5.0)],
170            // La `Ecuacion` tiene parámetros dinámicos (nombres `String`): no caben en
171            // esta firma `&'static str`; la UI los edita vía [`ecuacion_params`].
172            LeyKind::Ecuacion { .. } => vec![],
173        }
174    }
175
176    /// Fija el parámetro `i` (en el orden de [`params`](Self::params)) a `v`.
177    pub fn set_param(&mut self, i: usize, v: f32) {
178        match self {
179            LeyKind::Fluir { gravedad, horizontal } => match i {
180                0 => *gravedad = v,
181                1 => *horizontal = v,
182                _ => {}
183            },
184            LeyKind::Crecer { velocidad } => {
185                if i == 0 {
186                    *velocidad = v;
187                }
188            }
189            // Para `Ecuacion` se usa [`set_ecuacion_param`] (params dinámicos).
190            LeyKind::Ecuacion { .. } => {}
191        }
192    }
193
194    // --- Autoría de leyes por ecuación -------------------------------------
195
196    /// Parámetros de una ley `Ecuacion` como `(nombre, valor, min, max)` para armar
197    /// sliders. Vacío si no es una `Ecuacion`.
198    pub fn ecuacion_params(&self) -> Vec<(String, f32, f32, f32)> {
199        match self {
200            LeyKind::Ecuacion { params, .. } => {
201                params.iter().map(|p| (p.name.clone(), p.value, p.min, p.max)).collect()
202            }
203            _ => Vec::new(),
204        }
205    }
206
207    /// Valores de los parámetros de la `Ecuacion` en orden (los que consume el motor).
208    pub fn ecuacion_param_values(&self) -> Vec<f32> {
209        match self {
210            LeyKind::Ecuacion { params, .. } => params.iter().map(|p| p.value).collect(),
211            _ => Vec::new(),
212        }
213    }
214
215    /// Fija el valor del parámetro `i` de una ley `Ecuacion` (clampeado a su rango).
216    pub fn set_ecuacion_param(&mut self, i: usize, v: f32) {
217        if let LeyKind::Ecuacion { params, .. } = self {
218            if let Some(p) = params.get_mut(i) {
219                p.value = v.clamp(p.min, p.max);
220            }
221        }
222    }
223
224    /// Tabla de símbolos (campos + params por nombre) para parsear/imprimir sus
225    /// fórmulas. `None` si no es una `Ecuacion`.
226    pub fn ecuacion_symbols(&self) -> Option<Symbols> {
227        match self {
228            LeyKind::Ecuacion { campos, params, .. } => Some(Symbols {
229                campos: campos.iter().map(|c| c.name.clone()).collect(),
230                params: params.iter().map(|p| p.name.clone()).collect(),
231            }),
232            _ => None,
233        }
234    }
235
236    /// Compila una ley `Ecuacion` a un [`Program`] ejecutable: parsea cada fuente con
237    /// los símbolos de la ley y arma un `Δcampoₖ/dt` por campo. `None` si no es
238    /// `Ecuacion`; `Err(msg)` con el primer error de parseo (legible, ubicando el campo).
239    pub fn compile_ecuacion(&self) -> Option<Result<Program, String>> {
240        let LeyKind::Ecuacion { campos, fuentes, .. } = self else {
241            return None;
242        };
243        let sym = self.ecuacion_symbols()?;
244        let mut asignaciones = Vec::with_capacity(fuentes.len());
245        for (i, src) in fuentes.iter().enumerate() {
246            match Expr::parse(src, &sym) {
247                Ok(expr) => asignaciones.push(Assign { campo: i as u16, expr }),
248                Err(err) => {
249                    let campo = campos.get(i).map(|c| c.name.as_str()).unwrap_or("?");
250                    return Some(Err(format!("campo «{campo}»: {err}")));
251                }
252            }
253        }
254        Some(Ok(Program::compile(&asignaciones)))
255    }
256}
257
258/// Una **Ley**: nombre + tipo (con sus parámetros). El nivel más básico de la
259/// composición; los materiales la adoptan vía [`LeyUso`].
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct Ley {
262    pub id: u64,
263    pub name: String,
264    pub kind: LeyKind,
265}
266
267// =============================================================================
268//  Nivel 2 — Materiales
269// =============================================================================
270
271/// **Rol de un material**: relleno de terreno o un objeto colocable (con su forma).
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
273pub enum MatRole {
274    /// Relleno del terreno (suelo, acantilado, cumbre…).
275    Terreno,
276    /// Objeto colocado sobre el suelo (cactus, árbol, roca suelta…).
277    Objeto(Forma),
278}
279
280impl MatRole {
281    pub fn label(&self) -> &'static str {
282        match self {
283            MatRole::Terreno => "terreno",
284            MatRole::Objeto(_) => "objeto",
285        }
286    }
287}
288
289/// **Uso de una ley por un material**: a qué ley refiere y con qué parámetros
290/// concretos (overridean los defaults de la ley — «espesor del líquido», etc.).
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct LeyUso {
293    pub ley: u64,
294    pub params: Vec<f32>,
295}
296
297/// Un **material autorable**: color/grano (texturas a futuro) + rol + leyes
298/// aplicadas, con **herencia viva por padre**. Los campos visuales son `Option`:
299/// `None` = heredar del padre (o del builtin). [`Project::resolve_material`] aplana
300/// la cadena. Así «cactus amarillo» = hijo de «cactus verde» que sólo redefine el
301/// color.
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct MaterialDef {
304    pub id: u64,
305    pub name: String,
306    /// Material del que hereda lo no redefinido (`None` = raíz).
307    #[serde(default)]
308    pub parent: Option<u64>,
309    pub role: MatRole,
310    /// Color `[r,g,b]` en `[0,1]`; `None` = heredar.
311    #[serde(default)]
312    pub color: Option<[f32; 3]>,
313    /// Grano de materia `[0,1]`; `None` = heredar.
314    #[serde(default)]
315    pub grain: Option<f32>,
316    /// Brillo/reflectividad `[0,1]`; `None` = heredar (del padre o del builtin).
317    #[serde(default)]
318    pub brillo: Option<f32>,
319    /// **Rugosidad sub-voxel** `[0,~1.5]` (bump procedural): rompe la cara plana del
320    /// voxel a corta distancia. `None` = heredar (default efectivo 0 = liso). Es un
321    /// look GLOBAL del ray-march; el preview aplica el del material en foco (ver
322    /// `Project::detalle_material`). Roca alta, metal 0.
323    #[serde(default)]
324    pub rugosidad: Option<f32>,
325    /// **Bisel sub-voxel** `[0,1]` (suavizado por isosuperficie): pendientes
326    /// diagonales y esquinas biseladas. `None` = heredar (default 0 = voxel duro).
327    /// También look global del preview.
328    #[serde(default)]
329    pub bisel: Option<f32>,
330    /// Leyes que adopta, con sus parámetros.
331    #[serde(default)]
332    pub leyes: Vec<LeyUso>,
333    /// Tag de paleta semilla (de fábrica): deja que la IA/preset encuentre «la
334    /// arena» sin clavar un id. `None` en materiales hechos por el usuario.
335    #[serde(default)]
336    pub builtin: Option<Material>,
337}
338
339impl MaterialDef {
340    /// Material de fábrica a partir de una variante semilla (color/grano/rol).
341    pub fn from_builtin(id: u64, m: Material) -> Self {
342        let role = match m {
343            Material::Cactus => MatRole::Objeto(Forma::Columnar),
344            Material::Leaf => MatRole::Objeto(Forma::Arbol),
345            Material::Wheat => MatRole::Objeto(Forma::Trigo),
346            Material::Bloom => MatRole::Objeto(Forma::Lavanda),
347            _ => MatRole::Terreno,
348        };
349        let c = m.color();
350        Self {
351            id,
352            name: m.label().to_string(),
353            parent: None,
354            role,
355            color: Some([c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0]),
356            grain: Some(m.grain()),
357            brillo: None, // hereda del builtin (Material::brillo)
358            rugosidad: None,
359            bisel: None,
360            leyes: Vec::new(),
361            builtin: Some(m),
362        }
363    }
364}
365
366// =============================================================================
367//  Nivel 5 — Mundos
368// =============================================================================
369
370/// Un **Mundo**: semilla + uno o más biomas. La semilla es lo único «numérico» que
371/// el usuario teclea (con botón de random en la UI). El stage 1 genera con el primer
372/// bioma; la distribución multi-bioma queda como refinamiento.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct Mundo {
375    pub id: u64,
376    pub name: String,
377    pub seed: u32,
378    #[serde(default)]
379    pub biomas: Vec<u64>,
380    /// **Fuente de mapa real** (si el mundo se importó de un GeoJSON): persiste la
381    /// receta, no el grid. `None` = mundo procedural (bioma+semilla). Ver
382    /// [`MapaFuente`].
383    #[serde(default)]
384    pub mapa: Option<MapaFuente>,
385}
386
387/// **Fuente de un mundo importado de un mapa real**, persistible dentro del
388/// [`Project`]. Guarda el GeoJSON del visor (y un DEM `.pgm` opcional) **embebidos**
389/// —el documento queda autocontenido— para que el grid se **regenere** al abrir (un
390/// mundo es una receta, no un grid). El puente que lo materializa a `VoxelGrid` vive
391/// en el frontend (`nahual-geo-voxel`), no en este core: aquí sólo son datos.
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct MapaFuente {
394    /// GeoJSON crudo (una `FeatureCollection`) del mapa.
395    pub geojson: String,
396    /// DEM opcional (PGM binario P5) embebido; `None` = sin relieve por esta vía.
397    #[serde(default)]
398    pub pgm: Option<Vec<u8>>,
399    /// DEM opcional en **GeoTIFF** (`.tif`) embebido — georreferenciado por sí mismo
400    /// (tiene prioridad sobre `pgm`). `None` = sin relieve por esta vía.
401    #[serde(default)]
402    pub dem_tif: Option<Vec<u8>>,
403    /// Rango de altura `[0, rango]` (metros) al que se mapea el DEM **PGM** (el
404    /// GeoTIFF trae sus metros absolutos, no usa esto).
405    #[serde(default = "default_rango_dem")]
406    pub rango_dem_m: f32,
407}
408
409fn default_rango_dem() -> f32 {
410    300.0
411}
412
413/// Lo que el render necesita de un mundo: el bioma (relieve), la semilla y la paleta
414/// **ya resuelta** (colores concretos). Lo arma [`Project::render_mundo`].
415#[derive(Debug, Clone)]
416pub struct MundoRender {
417    pub bioma: Bioma,
418    pub seed: u32,
419    pub palette: BiomaPalette,
420}
421
422/// **Especificación serializable de un personaje**: lo que un editor/IA fija (edad
423/// + colores). Se materializa con [`to_actor`](Self::to_actor) en un [`Actor`]
424/// posable. Los colores son `[r, g, b]` en `[0,1]` (como [`Actor`]).
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct CharSpec {
427    /// Id estable (asignado por el [`Project`]); `0` hasta que se agrega.
428    #[serde(default)]
429    pub id: u64,
430    pub name: String,
431    pub age: Age,
432    pub skin: [f32; 3],
433    pub shirt: [f32; 3],
434    pub pants: [f32; 3],
435    /// **Pelo** `[r,g,b]` en `[0,1]`. `None` = derivado de la piel; `Some` = color explícito
436    /// (p.ej. muestreado FIEL de una foto). Serde default para compat con proyectos viejos.
437    #[serde(default)]
438    pub hair: Option<[f32; 3]>,
439    /// **Cuerpo**: `None` = humanoide (muñeco [`Actor`] rico: cara, gestos, cross-fade,
440    /// look-at); `Some(mov)` = una morfología arbitraria ([`Movimiento`]: rig +
441    /// andares por estado) — cuadrúpedo, ave, serpiente… Default `None` (compat).
442    #[serde(default)]
443    pub cuerpo: Option<Movimiento>,
444    /// **Conducta** (capa 3): cómo se mueve por el mundo (deambular/gregario/miedo).
445    /// Aplica a cualquier cuerpo (humanoide o rig).
446    #[serde(default)]
447    pub conducta: Conducta,
448}
449
450impl CharSpec {
451    /// Un personaje con la paleta por defecto de [`Actor::new`] a la edad dada.
452    /// `id = 0` hasta que el [`Project`] se lo asigna al agregarlo.
453    pub fn new(name: impl Into<String>, age: Age) -> Self {
454        let a = Actor::new(Vec3::ZERO, 0.0);
455        Self {
456            id: 0,
457            name: name.into(),
458            age,
459            skin: a.skin,
460            shirt: a.shirt,
461            pants: a.pants,
462            hair: None,
463            cuerpo: None,
464            conducta: Conducta::default(),
465        }
466    }
467
468    /// Nombre legible del cuerpo (para la UI).
469    pub fn cuerpo_label(&self) -> &str {
470        match &self.cuerpo {
471            None => "humanoide",
472            Some(m) => m.rig.nombre.as_str(),
473        }
474    }
475
476    /// Cicla el cuerpo: humanoide → cuadrúpedo → ave → serpiente → humanoide. Al
477    /// cambiar de rig, regenera andares por defecto (siempre en sync con la morfología).
478    pub fn cycle_cuerpo(&mut self) {
479        let presets = Rig::presets();
480        self.cuerpo = match &self.cuerpo {
481            None => Some(Movimiento::preset(presets[1].clone())), // humanoide → cuadrúpedo
482            Some(m) => {
483                let i = presets.iter().position(|p| p.nombre == m.rig.nombre).unwrap_or(0);
484                if i + 1 < presets.len() {
485                    Some(Movimiento::preset(presets[i + 1].clone()))
486                } else {
487                    None // serpiente → humanoide
488                }
489            }
490        };
491    }
492
493    /// **Meta de render** del ser en `pos`/`facing` para una animación: la matriz de
494    /// modelo + la malla. Humanoide (`rig = None`) usa el [`Actor`] rico (clip, fase,
495    /// look-at); un rig usa su [`Andar::caminar`] al andar (Walk/Run) o pose neutra si
496    /// no. Centraliza el branch para el preview y las escenas.
497    pub fn to_meta(
498        &self,
499        pos: Vec3,
500        facing: f32,
501        clip: Clip,
502        phase: f32,
503        look: Option<Vec3>,
504    ) -> (Mat4, Vec<Vertex3d>, Vec<u16>) {
505        match &self.cuerpo {
506            None => {
507                let mut a = self.to_actor(pos, facing);
508                a.set_clip(clip);
509                a.advance(phase);
510                a.look_at(look);
511                let (v, i) = a.mesh();
512                (a.model(), v, i)
513            }
514            Some(mov) => {
515                // El andar según el clip: locomoción (editable) o gesto (procedural).
516                let pose = match clip {
517                    Clip::Walk => mov.andares.caminar.pose(phase),
518                    Clip::Run => mov.andares.correr.pose(phase),
519                    Clip::Wave => Andar::saludar(&mov.rig).pose(phase),
520                    Clip::Point => Andar::senalar(&mov.rig).pose(phase),
521                    Clip::Cheer => Andar::festejar(&mov.rig).pose(phase),
522                    Clip::Idle => mov.andares.quieto.pose(phase),
523                };
524                let (v, i) = mov.rig.mesh(&pose, self.skin, self.shirt, self.pants);
525                let model = Mat4::from_translation(pos) * Mat4::from_rotation_y(facing);
526                (model, v, i)
527            }
528        }
529    }
530
531    /// Materializa el spec en un [`Actor`] parado en `pos` mirando a `facing`.
532    pub fn to_actor(&self, pos: Vec3, facing: f32) -> Actor {
533        Actor::new(pos, facing)
534            .with_age(self.age)
535            .with_colors(self.skin, self.shirt, self.pants)
536    }
537
538    /// **Estilo de personaje** (proporciones/rasgos anclados en literatura) derivado de la
539    /// edad. Ver [`EstiloSer`]. Futuro: guardarlo en el spec para elegir arquetipo a mano.
540    pub fn estilo(&self) -> EstiloSer {
541        EstiloSer::por_edad(self.age)
542    }
543
544    /// **Materializa el ser como VOLUMEN VOXEL redondeado** (no la malla de cajas del
545    /// [`Actor`]): el humanoide estilizado del kit ([`humanoide_voxel`]) en la pose del `clip`/
546    /// `phase`, coloreado por sus rasgos (piel/remera/pantalón), con el detalle sub-voxel
547    /// horneado. `altura` = alto en voxels. Se rinde con `bisel` + `silueta_por_voxel` +
548    /// AO baja para que el ray-marcher lo redondee a un look vinyl-toy/chibi (sin cajas, sin
549    /// valle). Sólo para el humanoide (`cuerpo = None`); un rig usa su propia malla.
550    pub fn to_voxel(&self, clip: Clip, phase: f32, altura: u32) -> VoxelGrid {
551        let pose = clip.pose(phase);
552        let col = match self.hair {
553            Some(h) => Colores::con_pelo(self.skin, self.shirt, self.pants, h),
554            None => Colores::nuevos(self.skin, self.shirt, self.pants),
555        };
556        humanoide_voxel(&self.estilo(), &pose, altura, col)
557    }
558}
559
560/// **Keyframe serializable** de un actor (espejo de [`ActorKey`]): dónde está en
561/// la grilla en `t`, y opcionalmente qué clip reproduce y hacia dónde mira.
562#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
563pub struct ActorKeySpec {
564    pub t: f32,
565    pub gx: f32,
566    pub gz: f32,
567    #[serde(default)]
568    pub clip: Option<Clip>,
569    #[serde(default)]
570    pub face: Option<f32>,
571}
572
573impl ActorKeySpec {
574    /// Compila a un [`ActorKey`] del director.
575    pub fn to_key(self) -> ActorKey {
576        let mut k = ActorKey::at(self.t, self.gx, self.gz);
577        if let Some(c) = self.clip {
578            k = k.play(c);
579        }
580        if let Some(f) = self.face {
581            k = k.facing(f);
582        }
583        k
584    }
585}
586
587/// **Actor de una escena**: qué personaje del proyecto lo interpreta (`character`,
588/// índice en [`Project::characters`]) y su guion de keyframes.
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct ActorSpec {
591    pub character: usize,
592    pub keys: Vec<ActorKeySpec>,
593    /// **Tasa de cuadros propia** del actor (`None` = fluido/nativo). Con un valor
594    /// bajo (12–15) el actor se anima *en doses* (stop-motion): es el sello que
595    /// separa al Héroe del Avatar. Ver [`ActorScript::quantize`].
596    #[serde(default)]
597    pub frame_rate: Option<u32>,
598}
599
600impl ActorSpec {
601    /// Compila los keyframes a un [`ActorScript`] reproducible (con su tasa de
602    /// cuadros propia, si la tiene).
603    pub fn to_script(&self) -> ActorScript {
604        ActorScript::new(self.keys.iter().map(|k| k.to_key()).collect())
605            .with_frame_rate(self.frame_rate)
606    }
607}
608
609/// **Tipo de plano** de cámara: un encuadre cinematográfico de alto nivel que se
610/// resuelve contra el **centroide del reparto** (no contra `eye/target` crudos), así
611/// es trivial de elegir y de generar por IA. [`resolve`](Self::resolve) produce la
612/// [`Camera3d`] del frame.
613#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
614pub enum ShotKind {
615    /// Establecedor: lejos y alto, presenta la escena.
616    Establishing,
617    /// Primer plano: cerca, a la altura del pecho.
618    CloseUp,
619    /// Lateral: desde el costado.
620    Side,
621    /// Órbita: gira lento alrededor del reparto.
622    Orbit,
623    /// **Contraluz**: cámara del lado opuesto al sol, mirando hacia él con el reparto
624    /// a contraluz (silueta en el tercio bajo) y el disco solar en lo alto del cuadro
625    /// → rim light + **god rays** (haces volumétricos). El plano "héroe" del motor.
626    Backlight,
627}
628
629impl ShotKind {
630    /// Todos los planos (para ciclar en un editor).
631    pub const ALL: [ShotKind; 5] = [
632        ShotKind::Establishing,
633        ShotKind::CloseUp,
634        ShotKind::Side,
635        ShotKind::Orbit,
636        ShotKind::Backlight,
637    ];
638
639    /// Nombre legible (español).
640    pub fn label(self) -> &'static str {
641        match self {
642            ShotKind::Establishing => "establecedor",
643            ShotKind::CloseUp => "primer plano",
644            ShotKind::Side => "lateral",
645            ShotKind::Orbit => "órbita",
646            ShotKind::Backlight => "contraluz",
647        }
648    }
649
650    /// El plano siguiente (cicla).
651    pub fn next(self) -> ShotKind {
652        let i = ShotKind::ALL.iter().position(|&k| k == self).unwrap_or(0);
653        ShotKind::ALL[(i + 1) % ShotKind::ALL.len()]
654    }
655
656    /// Resuelve la cámara del plano: mira a `look` (centroide del reparto, ya
657    /// elevado a la altura del pecho), con el ojo según el tipo, a distancia base
658    /// `d` (escala con el tamaño del reparto). `t` (seg) anima la órbita.
659    pub fn resolve(self, look: Vec3, d: f32, t: f32) -> Camera3d {
660        // `(eye, fov, target)` por tipo. Casi todos miran al reparto (`look`); el
661        // contraluz sube el objetivo hacia el sol para meterlo en el cuadro.
662        let (eye, fov, target) = match self {
663            ShotKind::Establishing => {
664                (look + Vec3::new(-0.5 * d, 0.9 * d, -1.6 * d), 50.0, look)
665            }
666            ShotKind::CloseUp => (look + Vec3::new(0.25 * d, 0.45 * d, -0.85 * d), 40.0, look),
667            ShotKind::Side => (look + Vec3::new(1.35 * d, 0.4 * d, 0.15 * d), 46.0, look),
668            ShotKind::Orbit => {
669                let a = t * 0.6;
670                (look + Vec3::new(a.cos() * 1.3 * d, 0.6 * d, a.sin() * 1.3 * d), 48.0, look)
671            }
672            ShotKind::Backlight => {
673                // Detrás del reparto respecto al sol (horizontal anti-sol), apenas
674                // elevado; el objetivo sube hacia el cielo → la cámara mira hacia el
675                // sol con el reparto a contraluz en el tercio bajo.
676                let s = Vec3::new(SCENE_SUN[0], SCENE_SUN[1], SCENE_SUN[2]).normalize();
677                let back = Vec3::new(-s.x, 0.0, -s.z).normalize();
678                let eye = look + back * (1.2 * d) + Vec3::new(0.0, 0.15 * d, 0.0);
679                let target = look + Vec3::new(0.0, 0.55 * d, 0.0);
680                (eye, 55.0, target)
681            }
682        };
683        Camera3d { eye, target, fovy_rad: fov_f32_to_rad(fov), ..Camera3d::default() }
684    }
685}
686
687/// Grados → radianes (helper local para no depender de glam en el call site).
688fn fov_f32_to_rad(deg: f32) -> f32 {
689    deg * std::f32::consts::PI / 180.0
690}
691
692/// Un **plano** de la escena: el tipo de encuadre y desde qué instante (seg) está
693/// activo. El plano vigente en `t` es el último con `start ≤ t` (corte duro entre
694/// planos).
695#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
696pub struct ShotSpec {
697    pub start: f32,
698    pub kind: ShotKind,
699}
700
701/// Qué clase de SUCESO se dicta en un punto de la escena. Se dictan desde
702/// adentro (caminando/volando el mundo) y se autoran aquí.
703#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
704pub enum TipoSuceso {
705    /// Aparece un ser (id en [`Project::seres`]; `0` = todavía por asignar).
706    Aparece(u64),
707    /// La cámara mira a este punto (una marca para dirigir un plano).
708    Camara,
709    /// Punto de interés genérico — "aquí pasa algo".
710    Marca,
711}
712
713/// Un SUCESO dictado en la escena: qué pasa, dónde y cuándo. El creador lo deja
714/// caer mientras recorre el mundo en primera persona.
715#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
716pub struct Suceso {
717    /// Instante en la escena (segundos).
718    pub t: f32,
719    /// Dónde, en coordenadas del mundo/grilla `[x, y, z]`.
720    pub pos: [f32; 3],
721    /// Qué clase de suceso.
722    pub tipo: TipoSuceso,
723    /// Nota libre del creador ("aquí despierta", "corte a primer plano"…).
724    pub nota: String,
725}
726
727/// **Especificación serializable de una escena**: el mundo de fondo (`world`,
728/// índice en [`Project::worlds`]), la duración, el reparto guionado y los **planos**
729/// de cámara. Es la versión editable/IA-emisible del [`Sequence`](crate::Sequence)
730/// del director; se compila con [`scripts`](Self::scripts) y se reproduce posando
731/// cada actor en `sample(t)` con la cámara del plano vigente.
732#[derive(Debug, Clone, Serialize, Deserialize)]
733pub struct SceneSpec {
734    /// Id estable (asignado por el [`Project`]).
735    #[serde(default)]
736    pub id: u64,
737    pub name: String,
738    /// El **mundo** de fondo (id en [`Project::mundos`]).
739    pub mundo: u64,
740    pub duration: f32,
741    pub actors: Vec<ActorSpec>,
742    #[serde(default)]
743    pub shots: Vec<ShotSpec>,
744    /// **Cámara en mano**: intensidad del temblor orgánico (`0` = trípode fijo,
745    /// look de dron; `~1` = respiración/pulso de camarógrafo). Ensucia la cámara
746    /// matemáticamente perfecta del motor — es el sello que mete al espectador en
747    /// el "barro" de la escena. Ver [`handheld_shake`].
748    #[serde(default)]
749    pub handheld: f32,
750    /// **Atmósfera** de apertura: la bruma + el fundido desde negro. `None` =
751    /// aire despejado, sin fundido (compatibilidad con escenas viejas).
752    #[serde(default)]
753    pub atmosfera: Option<crate::apertura::Atmosfera>,
754    /// **Caída** de apertura: la cámara pura cae del cielo (sin cuerpo) hasta
755    /// aterrizar. `None` = cámara normal por planos.
756    #[serde(default)]
757    pub caida: Option<crate::apertura::Caida>,
758    /// **Sucesos** dictados desde adentro: qué pasa, dónde y cuándo.
759    #[serde(default)]
760    pub sucesos: Vec<Suceso>,
761}
762
763/// **Temblor de cámara en mano**, determinista (función pura de `t` → la peli sale
764/// reproducible cuadro a cuadro). Suma de senos en frecuencias inconmensurables:
765/// una **respiración** lenta (bob vertical) + un **micro-pulso** rápido en los tres
766/// ejes para el ojo, y una **deriva** aún más lenta para el objetivo (el encuadre
767/// flota, no sólo tiembla). `amt ≤ 0` → sin offset (trípode). Devuelve
768/// `(offset_ojo, offset_objetivo)` en unidades de mundo, escalado un poco con la
769/// distancia `d` del plano para que también respire en planos lejanos.
770pub fn handheld_shake(t: f32, amt: f32, d: f32) -> (Vec3, Vec3) {
771    if amt <= 0.0 {
772        return (Vec3::ZERO, Vec3::ZERO);
773    }
774    let scale = amt * (1.0 + d * 0.03);
775    // Respiración (bob) + micro-pulso por eje (fases y frecuencias dispares).
776    let breath = (t * 1.7).sin() * 0.6 + (t * 0.9).sin() * 0.4;
777    let jx = (t * 9.3).sin() * 0.5 + (t * 4.7 + 1.3).sin() * 0.5;
778    let jy = (t * 8.1 + 2.1).sin() * 0.5 + (t * 5.3 + 0.7).sin() * 0.5;
779    let jz = (t * 7.4 + 0.4).sin() * 0.5 + (t * 3.9 + 2.7).sin() * 0.5;
780    let eye = Vec3::new(jx * 0.10, breath * 0.12 + jy * 0.08, jz * 0.10) * scale;
781    // Deriva del objetivo: más lenta y desfasada → el cuadro "busca" al sujeto.
782    let tgt = Vec3::new(
783        (t * 1.3 + 0.5).sin() * 0.06,
784        (t * 1.1 + 1.9).sin() * 0.05,
785        0.0,
786    ) * scale;
787    (eye, tgt)
788}
789
790impl SceneSpec {
791    /// Los guiones de los actores, listos para `sample(t)`.
792    pub fn scripts(&self) -> Vec<ActorScript> {
793        self.actors.iter().map(|a| a.to_script()).collect()
794    }
795
796    /// Los **instantes (seg) que merecen un acento musical**: los cortes de cámara
797    /// (inicio de cada plano salvo el primero) y los **gestos** de los actores (keys
798    /// con un clip *emote*). Es lo que deja caer la banda sonora *sobre la acción*.
799    /// Ordenados, sin repetir (dos a menos de `EPS` se funden). Espeja
800    /// [`Sequence::beat_times`](crate::Sequence::beat_times).
801    pub fn beat_times(&self) -> Vec<f32> {
802        const EPS: f32 = 0.05;
803        let mut ts: Vec<f32> = Vec::new();
804        for s in self.shots.iter().skip(1) {
805            ts.push(s.start);
806        }
807        for a in &self.actors {
808            for k in &a.keys {
809                if k.clip.is_some_and(|c| c.is_emote()) {
810                    ts.push(k.t);
811                }
812            }
813        }
814        ts.retain(|&t| t >= 0.0 && t <= self.duration + EPS);
815        ts.sort_by(f32::total_cmp);
816        ts.dedup_by(|a, b| (*a - *b).abs() < EPS);
817        ts
818    }
819
820    /// La **cámara de la escena** en `t`: resuelve el plano vigente mirando a
821    /// `look` (centroide del reparto) a distancia `d`, y le suma el temblor de
822    /// **cámara en mano** ([`handheld_shake`]) según [`Self::handheld`]. Es el
823    /// único punto por el que deberían pasar el preview y el export para que el
824    /// sello de cámara salga igual en ambos.
825    pub fn camera_at(&self, look: Vec3, d: f32, t: f32) -> Camera3d {
826        // Apertura por CAÍDA (cámara pura, sin cuerpo): mientras dura la caída, la
827        // cámara desciende del cielo hacia `look`; al aterrizar, cede a los planos.
828        if let Some(caida) = self.caida {
829            if t < caida.duracion {
830                let mut cam = caida.camara_desde(look, t);
831                let (eo, to) = handheld_shake(t, self.handheld, d);
832                cam.eye += eo;
833                cam.target += to;
834                return cam;
835            }
836        }
837        let mut cam = self.active_shot(t).resolve(look, d, t);
838        let (eo, to) = handheld_shake(t, self.handheld, d);
839        cam.eye += eo;
840        cam.target += to;
841        cam
842    }
843
844    /// El plano vigente en `t` (el último con `start ≤ t`); `Establishing` si no
845    /// hay planos definidos.
846    pub fn active_shot(&self, t: f32) -> ShotKind {
847        self.shots
848            .iter()
849            .filter(|s| s.start <= t)
850            .last()
851            .map(|s| s.kind)
852            .unwrap_or(ShotKind::Establishing)
853    }
854
855    /// **Escena patrón "entran y saludan"**: `n` actores entran caminando por el
856    /// centro del mundo de izquierda a derecha, se giran y hacen `gesture`. Coords
857    /// de grilla (la altura del terreno se aplica al reproducir). La base tanto del
858    /// arranque como de la generación por IA.
859    pub fn walk_and_emote(
860        name: impl Into<String>,
861        mundo: u64,
862        n: usize,
863        gesture: Clip,
864        dim: [u32; 3],
865    ) -> Self {
866        use std::f32::consts::{FRAC_PI_2, PI};
867        let n = n.clamp(1, 5);
868        let margin = 18.0_f32;
869        let gx0 = margin;
870        let gx1 = (dim[0] as f32 - margin).max(gx0 + 1.0);
871        let cz = dim[2] as f32 * 0.5;
872        let (t_walk, t_turn, dur) = (2.6_f32, 3.0_f32, 5.6_f32);
873
874        let mut actors = Vec::with_capacity(n);
875        for i in 0..n {
876            let off = (i as f32 - (n as f32 - 1.0) / 2.0) * 3.0;
877            let gz = cz + off;
878            actors.push(ActorSpec {
879                character: i,
880                keys: vec![
881                    ActorKeySpec { t: 0.0, gx: gx0, gz, clip: None, face: None },
882                    ActorKeySpec { t: t_walk, gx: gx1, gz, clip: None, face: Some(FRAC_PI_2) },
883                    ActorKeySpec { t: t_turn, gx: gx1, gz, clip: Some(gesture), face: Some(PI) },
884                    ActorKeySpec { t: dur, gx: gx1, gz, clip: Some(gesture), face: Some(PI) },
885                ],
886                // El **Héroe** (primer actor) se anima en doses (12 fps): se mueve a
887                // tirones, pesado, contra los demás (Avatares) que van fluidos. Es el
888                // sello de animación, visible ya en la escena de arranque.
889                frame_rate: if i == 0 { Some(12) } else { None },
890            });
891        }
892        // Tres planos: establecedor, **contraluz** a mitad de la caminata (el reparto
893        // entra al sol → luce los god rays), y primer plano en el gesto.
894        let shots = vec![
895            ShotSpec { start: 0.0, kind: ShotKind::Establishing },
896            ShotSpec { start: t_walk * 0.5, kind: ShotKind::Backlight },
897            ShotSpec { start: t_turn, kind: ShotKind::CloseUp },
898        ];
899        // Cámara en mano suave por defecto: el sello se ve sin tener que pedirlo.
900        Self {
901            id: 0,
902            name: name.into(),
903            mundo,
904            duration: dur,
905            actors,
906            shots,
907            handheld: 0.7,
908            atmosfera: None,
909            caida: None,
910            sucesos: Vec::new(),
911        }
912    }
913}
914
915/// El **proyecto**: la bolsa de artefactos del creador, por niveles de composición
916/// (leyes → materiales → seres → biomas → mundos → escenas). Cada item lleva un `id`
917/// estable (separado del nombre) para referenciarse, renombrarse y duplicarse.
918/// Vacío por defecto; [`starter`](Self::starter) trae algo que tocar.
919#[derive(Debug, Clone, Default, Serialize, Deserialize)]
920pub struct Project {
921    /// Contador de ids; `0` queda reservado como «ninguno».
922    #[serde(default)]
923    pub next_id: u64,
924    #[serde(default)]
925    pub leyes: Vec<Ley>,
926    #[serde(default)]
927    pub materiales: Vec<MaterialDef>,
928    /// Los **seres** (personajes); el tipo se llama `CharSpec` por historia.
929    #[serde(default)]
930    pub seres: Vec<CharSpec>,
931    #[serde(default)]
932    pub biomas: Vec<Bioma>,
933    #[serde(default)]
934    pub mundos: Vec<Mundo>,
935    #[serde(default)]
936    pub escenas: Vec<SceneSpec>,
937    /// Interiores amueblables (editor de interiores).
938    #[serde(default)]
939    pub interiores: Vec<crate::interior::Interior>,
940}
941
942impl Project {
943    /// Reserva el próximo id estable.
944    pub fn alloc_id(&mut self) -> u64 {
945        if self.next_id == 0 {
946            self.next_id = 1;
947        }
948        let id = self.next_id;
949        self.next_id += 1;
950        id
951    }
952
953    /// Proyecto de arranque: leyes base, los 6 materiales semilla, un trío de seres,
954    /// los biomas desierto/pradera y dos mundos que los envuelven, más la escena demo.
955    pub fn starter() -> Self {
956        let mut p = Project { next_id: 1, ..Default::default() };
957
958        // --- Leyes base ---
959        let ley_fluir = p.alloc_id();
960        p.leyes.push(Ley {
961            id: ley_fluir,
962            name: "fluir".into(),
963            kind: LeyKind::Fluir { gravedad: 1.0, horizontal: 0.6 },
964        });
965        let ley_crecer = p.alloc_id();
966        p.leyes.push(Ley {
967            id: ley_crecer,
968            name: "crecer".into(),
969            kind: LeyKind::Crecer { velocidad: 1.0 },
970        });
971
972        // --- Materiales semilla (arena/pasto/roca/nieve/agua/cactus) ---
973        let mut mat_ids = std::collections::HashMap::new();
974        for m in Material::ALL {
975            let id = p.alloc_id();
976            let mut def = MaterialDef::from_builtin(id, m);
977            // El agua fluye; el cactus crece — leyes de muestra (sin simular aún).
978            match m {
979                Material::Water => def.leyes.push(LeyUso { ley: ley_fluir, params: vec![1.0, 0.6] }),
980                Material::Cactus => def.leyes.push(LeyUso { ley: ley_crecer, params: vec![1.0] }),
981                _ => {}
982            }
983            mat_ids.insert(m, id);
984            p.materiales.push(def);
985        }
986        let mid = |m: Material| mat_ids[&m];
987
988        // --- Seres ---
989        let mut sere_ids = Vec::new();
990        for (name, skin, shirt, pants) in [
991            ("rojo", [0.90, 0.72, 0.58], [0.82, 0.28, 0.26], [0.20, 0.20, 0.28]),
992            ("azul", [0.86, 0.68, 0.54], [0.22, 0.55, 0.78], [0.18, 0.20, 0.24]),
993            ("amarillo", [0.92, 0.78, 0.62], [0.92, 0.80, 0.30], [0.26, 0.22, 0.20]),
994        ] {
995            let id = p.alloc_id();
996            sere_ids.push(id);
997            p.seres.push(CharSpec {
998                id,
999                name: name.into(),
1000                age: Age::Adult,
1001                skin,
1002                shirt,
1003                pants,
1004                hair: None,
1005                cuerpo: None,
1006                conducta: Conducta::default(),
1007            });
1008        }
1009
1010        // --- Biomas ---
1011        let bioma_desierto = p.alloc_id();
1012        p.biomas.push(Bioma {
1013            id: bioma_desierto,
1014            name: "desierto".into(),
1015            base: 0.30,
1016            dune: 0.05,
1017            relief: 0.45,
1018            mountains: 0.12,
1019            water_level: 0.26,
1020            rivers: 0.18,
1021            peak_at: 1.0,
1022            ground: mid(Material::Sand),
1023            cliff: mid(Material::Rock),
1024            peak: None,
1025            objetos: vec![
1026                crate::worldgen::ObjetoUso { material: mid(Material::Cactus), densidad: 0.010, forma: Forma::Columnar },
1027                // Palmeras ralas (oasis): follaje verde sobre tronco leñoso.
1028                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.004, forma: Forma::Palmera },
1029            ],
1030            minerales: vec![
1031                MineralUso::of(Mineral::Sal),
1032                MineralUso::of(Mineral::Cobre),
1033                MineralUso::of(Mineral::Hierro),
1034                MineralUso::of(Mineral::Oro),
1035                MineralUso::of(Mineral::Rubi),
1036            ],
1037            densidad_urbana: 0.4,
1038            casas: Vec::new(),
1039            fauna: vec![
1040                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Conejo, densidad: 0.25 },
1041                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Serpiente, densidad: 0.3 },
1042                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Zorro, densidad: 0.2 },
1043                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Camello, densidad: 0.2 },
1044                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Lagarto, densidad: 0.3 },
1045                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Escorpion, densidad: 0.25 },
1046            ],
1047            seres: vec![crate::worldgen::SereUso { sere: sere_ids[0], probabilidad: 0.5 }],
1048        });
1049        let bioma_pradera = p.alloc_id();
1050        p.biomas.push(Bioma {
1051            id: bioma_pradera,
1052            name: "pradera".into(),
1053            base: 0.22,
1054            dune: 0.10,
1055            relief: 0.7,
1056            mountains: 0.5,
1057            water_level: 0.30,
1058            rivers: 0.25,
1059            peak_at: 0.80,
1060            ground: mid(Material::Grass),
1061            cliff: mid(Material::Rock),
1062            peak: Some(mid(Material::Snow)),
1063            // Vegetación de pradera: árboles y arbustos ralos sobre un manto de grama
1064            // con flores. El orden importa — la primera forma que brota gana la columna,
1065            // así los árboles no quedan tapados por la grama densa.
1066            objetos: vec![
1067                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.008, forma: Forma::Arbol },
1068                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.012, forma: Forma::Arbusto },
1069                crate::worldgen::ObjetoUso { material: mid(Material::Wheat), densidad: 0.05, forma: Forma::Trigo },
1070                crate::worldgen::ObjetoUso { material: mid(Material::Bloom), densidad: 0.020, forma: Forma::Lavanda },
1071                crate::worldgen::ObjetoUso { material: mid(Material::Bloom), densidad: 0.012, forma: Forma::Flor },
1072                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.010, forma: Forma::Junco },
1073                crate::worldgen::ObjetoUso { material: mid(Material::Grass), densidad: 0.05, forma: Forma::Pasto },
1074            ],
1075            minerales: vec![
1076                MineralUso::of(Mineral::Carbon),
1077                MineralUso::of(Mineral::Hierro),
1078                MineralUso::of(Mineral::Cobre),
1079                MineralUso::of(Mineral::Cuarzo),
1080            ],
1081            densidad_urbana: 0.4,
1082            casas: Vec::new(),
1083            fauna: vec![
1084                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Vaca, densidad: 0.35 },
1085                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Oveja, densidad: 0.35 },
1086                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Conejo, densidad: 0.4 },
1087                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Mariposa, densidad: 0.5 },
1088                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Pato, densidad: 0.3 },
1089                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Cabra, densidad: 0.25 },
1090                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Burro, densidad: 0.15 },
1091            ],
1092            seres: vec![
1093                crate::worldgen::SereUso { sere: sere_ids[1], probabilidad: 0.5 },
1094                crate::worldgen::SereUso { sere: sere_ids[2], probabilidad: 0.4 },
1095            ],
1096        });
1097
1098        let bioma_bosque = p.alloc_id();
1099        p.biomas.push(Bioma {
1100            id: bioma_bosque,
1101            name: "bosque".into(),
1102            base: 0.24,
1103            dune: 0.06,
1104            relief: 0.6,
1105            mountains: 0.35,
1106            water_level: 0.28,
1107            rivers: 0.22,
1108            peak_at: 0.85,
1109            ground: mid(Material::Grass),
1110            cliff: mid(Material::Rock),
1111            peak: Some(mid(Material::Snow)),
1112            // Bosque denso: robles y árboles + coníferas, con tocones, hongos, helechos
1113            // y grama de sotobosque.
1114            objetos: vec![
1115                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.015, forma: Forma::Roble },
1116                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.040, forma: Forma::Arbol },
1117                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.030, forma: Forma::Conifera },
1118                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.025, forma: Forma::Arbusto },
1119                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.006, forma: Forma::Tocon },
1120                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.012, forma: Forma::Hongo },
1121                crate::worldgen::ObjetoUso { material: mid(Material::Cactus), densidad: 0.025, forma: Forma::Helecho },
1122                crate::worldgen::ObjetoUso { material: mid(Material::Grass), densidad: 0.04, forma: Forma::Pasto },
1123            ],
1124            minerales: vec![
1125                MineralUso::of(Mineral::Carbon),
1126                MineralUso::of(Mineral::Plata),
1127                MineralUso::of(Mineral::Amatista),
1128                MineralUso::of(Mineral::Lapis),
1129            ],
1130            densidad_urbana: 0.4,
1131            casas: Vec::new(),
1132            fauna: vec![
1133                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Ciervo, densidad: 0.4 },
1134                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Zorro, densidad: 0.3 },
1135                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Oso, densidad: 0.15 },
1136                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Conejo, densidad: 0.4 },
1137                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Ardilla, densidad: 0.35 },
1138                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Buho, densidad: 0.2 },
1139                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Erizo, densidad: 0.25 },
1140            ],
1141            seres: vec![crate::worldgen::SereUso { sere: sere_ids[1], probabilidad: 0.4 }],
1142        });
1143
1144        let bioma_selva = p.alloc_id();
1145        p.biomas.push(Bioma {
1146            id: bioma_selva,
1147            name: "selva".into(),
1148            base: 0.26,
1149            dune: 0.05,
1150            relief: 0.55,
1151            mountains: 0.30,
1152            water_level: 0.34,
1153            rivers: 0.4,
1154            peak_at: 0.9,
1155            ground: mid(Material::Grass),
1156            cliff: mid(Material::Rock),
1157            peak: None,
1158            // Selva húmeda: dosel de árboles, bambú, plataneras, helechos y hongos.
1159            objetos: vec![
1160                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.050, forma: Forma::Arbol },
1161                crate::worldgen::ObjetoUso { material: mid(Material::Cactus), densidad: 0.050, forma: Forma::Bambu },
1162                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.030, forma: Forma::Platanera },
1163                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.030, forma: Forma::Arbusto },
1164                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.012, forma: Forma::Hongo },
1165                crate::worldgen::ObjetoUso { material: mid(Material::Cactus), densidad: 0.045, forma: Forma::Helecho },
1166                crate::worldgen::ObjetoUso { material: mid(Material::Grass), densidad: 0.04, forma: Forma::Pasto },
1167            ],
1168            minerales: vec![
1169                MineralUso::of(Mineral::Esmeralda),
1170                MineralUso::of(Mineral::Oro),
1171                MineralUso::of(Mineral::Diamante),
1172                MineralUso::of(Mineral::Cobre),
1173            ],
1174            densidad_urbana: 0.4,
1175            casas: Vec::new(),
1176            fauna: vec![
1177                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Rana, densidad: 0.4 },
1178                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Serpiente, densidad: 0.3 },
1179                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Pajaro, densidad: 0.4 },
1180                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Mariposa, densidad: 0.5 },
1181                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Mono, densidad: 0.35 },
1182                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Loro, densidad: 0.35 },
1183                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Tigre, densidad: 0.15 },
1184                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Elefante, densidad: 0.12 },
1185                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Cocodrilo, densidad: 0.15 },
1186            ],
1187            seres: vec![crate::worldgen::SereUso { sere: sere_ids[2], probabilidad: 0.4 }],
1188        });
1189
1190        let bioma_volcan = p.alloc_id();
1191        p.biomas.push(Bioma {
1192            id: bioma_volcan,
1193            name: "volcán".into(),
1194            base: 0.20,
1195            dune: 0.04,
1196            relief: 0.9,
1197            mountains: 0.85,
1198            water_level: 0.0,
1199            rivers: 0.0,
1200            peak_at: 0.72,
1201            // La tanda nueva de materiales: laderas de basalto, cumbre de lava.
1202            ground: mid(Material::Basalto),
1203            cliff: mid(Material::Basalto),
1204            peak: Some(mid(Material::Lava)),
1205            objetos: vec![],
1206            // Veta rica: el fuego trae metales, gemas rojas y exóticos volcánicos.
1207            minerales: vec![
1208                MineralUso::of(Mineral::Hierro),
1209                MineralUso::of(Mineral::Oro),
1210                MineralUso::of(Mineral::Rubi),
1211                MineralUso::of(Mineral::Obsidiana),
1212                MineralUso::of(Mineral::Cinabrio),
1213            ],
1214            densidad_urbana: 0.4,
1215            casas: Vec::new(),
1216            fauna: vec![],
1217            seres: vec![],
1218        });
1219
1220        let bioma_gruta = p.alloc_id();
1221        p.biomas.push(Bioma {
1222            id: bioma_gruta,
1223            name: "gruta de cristales".into(),
1224            base: 0.30,
1225            dune: 0.05,
1226            relief: 0.8,
1227            mountains: 0.7,
1228            water_level: 0.10,
1229            rivers: 0.05,
1230            peak_at: 0.9,
1231            // Roca clara (granito) para que resalten las gemas exóticas.
1232            ground: mid(Material::Granito),
1233            cliff: mid(Material::Granito),
1234            peak: None,
1235            objetos: vec![],
1236            // Vitrina de exóticos: geodas, cristales y vetas de colores raros.
1237            minerales: vec![
1238                MineralUso::of(Mineral::Amatista),
1239                MineralUso::of(Mineral::Zafiro),
1240                MineralUso::of(Mineral::Topacio),
1241                MineralUso::of(Mineral::Turquesa),
1242                MineralUso::of(Mineral::Opalo),
1243                MineralUso::of(Mineral::Fluorita),
1244                MineralUso::of(Mineral::Jade),
1245                MineralUso::of(Mineral::Bismuto),
1246                MineralUso::of(Mineral::Uraninita),
1247            ],
1248            densidad_urbana: 0.4,
1249            casas: Vec::new(),
1250            fauna: vec![],
1251            seres: vec![],
1252        });
1253
1254        let bioma_pueblo = p.alloc_id();
1255        p.biomas.push(Bioma {
1256            id: bioma_pueblo,
1257            name: "pueblo".into(),
1258            base: 0.16,
1259            dune: 0.03,
1260            relief: 0.22,
1261            mountains: 0.12,
1262            water_level: 0.0,
1263            rivers: 0.0,
1264            peak_at: 1.0,
1265            ground: mid(Material::Grass),
1266            cliff: mid(Material::Rock),
1267            peak: None,
1268            objetos: vec![
1269                crate::worldgen::ObjetoUso { material: mid(Material::Leaf), densidad: 0.006, forma: Forma::Arbol },
1270                crate::worldgen::ObjetoUso { material: mid(Material::Grass), densidad: 0.04, forma: Forma::Pasto },
1271            ],
1272            minerales: vec![],
1273            // Un pueblo mezclado, esparcido por la semilla del mundo.
1274            densidad_urbana: 0.55,
1275            casas: vec![
1276                CasaUso { tipo: CasaTipo::Cabana, densidad: 0.35 },
1277                CasaUso { tipo: CasaTipo::Moderna, densidad: 0.3 },
1278                CasaUso { tipo: CasaTipo::Flintstone, densidad: 0.2 },
1279                CasaUso { tipo: CasaTipo::Hobbit, densidad: 0.2 },
1280                CasaUso { tipo: CasaTipo::Edificio, densidad: 0.15 },
1281            ],
1282            fauna: vec![
1283                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Gallina, densidad: 0.5 },
1284                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Cerdo, densidad: 0.3 },
1285                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Vaca, densidad: 0.3 },
1286                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Pato, densidad: 0.3 },
1287                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Cabra, densidad: 0.2 },
1288                crate::bicho::BichoUso { bicho: crate::bicho::Bicho::Burro, densidad: 0.15 },
1289            ],
1290            seres: vec![],
1291        });
1292
1293        // --- Mundos ---
1294        let mundo_desierto = p.alloc_id();
1295        p.mundos.push(Mundo {
1296            id: mundo_desierto,
1297            name: "desierto".into(),
1298            seed: 1337,
1299            biomas: vec![bioma_desierto],
1300            mapa: None,
1301        });
1302        let mundo_pradera = p.alloc_id();
1303        p.mundos.push(Mundo {
1304            id: mundo_pradera,
1305            name: "pradera".into(),
1306            seed: 1337,
1307            biomas: vec![bioma_pradera],
1308            mapa: None,
1309        });
1310        let mundo_bosque = p.alloc_id();
1311        p.mundos.push(Mundo {
1312            id: mundo_bosque,
1313            name: "bosque".into(),
1314            seed: 2027,
1315            biomas: vec![bioma_bosque],
1316            mapa: None,
1317        });
1318        let mundo_selva = p.alloc_id();
1319        p.mundos.push(Mundo {
1320            id: mundo_selva,
1321            name: "selva".into(),
1322            seed: 3141,
1323            biomas: vec![bioma_selva],
1324            mapa: None,
1325        });
1326        let mundo_volcan = p.alloc_id();
1327        p.mundos.push(Mundo {
1328            id: mundo_volcan,
1329            name: "volcán".into(),
1330            seed: 777,
1331            biomas: vec![bioma_volcan],
1332            mapa: None,
1333        });
1334        let mundo_gruta = p.alloc_id();
1335        p.mundos.push(Mundo {
1336            id: mundo_gruta,
1337            name: "gruta de cristales".into(),
1338            seed: 2718,
1339            biomas: vec![bioma_gruta],
1340            mapa: None,
1341        });
1342        let mundo_pueblo = p.alloc_id();
1343        p.mundos.push(Mundo {
1344            id: mundo_pueblo,
1345            name: "pueblo".into(),
1346            seed: 1234,
1347            biomas: vec![bioma_pueblo],
1348            mapa: None,
1349        });
1350
1351        // --- Escena demo ---
1352        let dim = world_dim(PREVIEW_DIM_XZ);
1353        let mut escena = SceneSpec::walk_and_emote("saludo en el desierto", mundo_desierto, 3, Clip::Wave, dim);
1354        escena.id = p.alloc_id();
1355        p.escenas.push(escena);
1356
1357        // --- Interior demo (sala amueblada) ---
1358        let mut sala = crate::interior::Interior::nuevo(p.alloc_id(), "sala");
1359        sala.colocar(crate::Mueble::Alfombra, 12, 10, 0);
1360        sala.colocar(crate::Mueble::Sofa, 12, 6, 0);
1361        sala.colocar(crate::Mueble::Mesa, 12, 10, 0);
1362        sala.colocar(crate::Mueble::Silla, 9, 10, 1);
1363        sala.colocar(crate::Mueble::Estante, 4, 3, 0);
1364        sala.colocar(crate::Mueble::Lampara, 20, 4, 0);
1365        sala.colocar(crate::Mueble::Maceta, 4, 16, 0);
1366        sala.colocar(crate::Mueble::Ficus, 20, 16, 0);
1367        sala.colocar(crate::Mueble::Cuadro, 12, 1, 0);
1368        sala.colocar(crate::Mueble::Espejo, 8, 1, 0);
1369        sala.colocar(crate::Mueble::Colgante, 16, 2, 0);
1370        sala.colocar(crate::Mueble::Aplique, 1, 8, 1);
1371        p.interiores.push(sala);
1372
1373        p
1374    }
1375
1376    // --- Búsquedas por id ---
1377    pub fn ley(&self, id: u64) -> Option<&Ley> {
1378        self.leyes.iter().find(|x| x.id == id)
1379    }
1380    pub fn material(&self, id: u64) -> Option<&MaterialDef> {
1381        self.materiales.iter().find(|x| x.id == id)
1382    }
1383    pub fn sere(&self, id: u64) -> Option<&CharSpec> {
1384        self.seres.iter().find(|x| x.id == id)
1385    }
1386    pub fn bioma(&self, id: u64) -> Option<&Bioma> {
1387        self.biomas.iter().find(|x| x.id == id)
1388    }
1389    pub fn mundo(&self, id: u64) -> Option<&Mundo> {
1390        self.mundos.iter().find(|x| x.id == id)
1391    }
1392
1393    /// Id del material de fábrica de la variante `m`, si está sembrado (para que la
1394    /// IA/presets encuentren «la arena» sin clavar números).
1395    pub fn material_id_for(&self, m: Material) -> Option<u64> {
1396        self.materiales.iter().find(|d| d.builtin == Some(m)).map(|d| d.id)
1397    }
1398
1399    /// **Resuelve un material** aplanando la cadena de herencia: el primer `Some`
1400    /// hacia arriba gana; si nada lo define, cae al color/grano del `builtin`, y por
1401    /// último a un gris neutro. Corta a profundidad 32 (anti-ciclo).
1402    pub fn resolve_material(&self, id: u64) -> ResolvedMaterial {
1403        let mut color: Option<[f32; 3]> = None;
1404        let mut grain: Option<f32> = None;
1405        let mut brillo: Option<f32> = None;
1406        let mut builtin: Option<Material> = None;
1407        let mut cur = Some(id);
1408        let mut depth = 0;
1409        while let Some(cid) = cur {
1410            if depth > 32 {
1411                break;
1412            }
1413            let Some(m) = self.material(cid) else { break };
1414            if color.is_none() {
1415                color = m.color;
1416            }
1417            if grain.is_none() {
1418                grain = m.grain;
1419            }
1420            if brillo.is_none() {
1421                brillo = m.brillo;
1422            }
1423            if builtin.is_none() {
1424                builtin = m.builtin;
1425            }
1426            cur = m.parent;
1427            depth += 1;
1428        }
1429        let color_u8 = color
1430            .map(|c| {
1431                [
1432                    (c[0].clamp(0.0, 1.0) * 255.0) as u8,
1433                    (c[1].clamp(0.0, 1.0) * 255.0) as u8,
1434                    (c[2].clamp(0.0, 1.0) * 255.0) as u8,
1435                ]
1436            })
1437            .or_else(|| builtin.map(|b| b.color()))
1438            .unwrap_or([150, 150, 150]);
1439        let grain = grain.or_else(|| builtin.map(|b| b.grain())).unwrap_or(0.0);
1440        let brillo = brillo.or_else(|| builtin.map(|b| b.brillo())).unwrap_or(0.0);
1441        // El detalle sub-voxel (rugosidad/bisel) viaja POR VOXEL con el material, así
1442        // dos materiales del bioma se ven distinto a la vez (lo que el look global no
1443        // podía). Aplana la herencia igual que color/brillo (`detalle_material`).
1444        ResolvedMaterial::new_b(color_u8, grain, brillo).con_detalle(self.detalle_material(id))
1445    }
1446
1447    /// **Detalle sub-voxel resuelto** `(rugosidad, bisel)` de un material, aplanando
1448    /// la herencia (primer `Some` hacia arriba gana; default `0.0` = liso/duro). Es un
1449    /// look GLOBAL del ray-march (no hay id de material por voxel todavía), así que el
1450    /// preview aplica el del material en foco. Espeja [`Self::resolve_material`].
1451    pub fn detalle_material(&self, id: u64) -> (f32, f32) {
1452        let mut rugosidad: Option<f32> = None;
1453        let mut bisel: Option<f32> = None;
1454        let mut cur = Some(id);
1455        let mut depth = 0;
1456        while let Some(cid) = cur {
1457            if depth > 32 {
1458                break;
1459            }
1460            let Some(m) = self.material(cid) else { break };
1461            if rugosidad.is_none() {
1462                rugosidad = m.rugosidad;
1463            }
1464            if bisel.is_none() {
1465                bisel = m.bisel;
1466            }
1467            cur = m.parent;
1468            depth += 1;
1469        }
1470        (rugosidad.unwrap_or(0.0).max(0.0), bisel.unwrap_or(0.0).clamp(0.0, 1.0))
1471    }
1472
1473    /// **Leyes efectivas** de un material: las propias + las heredadas de sus padres,
1474    /// cada una con sus parámetros aplicados (la `LeyUso.params` overridea los
1475    /// defaults de la ley). Es lo que dispara la simulación: un material *fluye*
1476    /// porque tiene una [`LeyKind::Fluir`], *crece* porque tiene [`LeyKind::Crecer`].
1477    pub fn resolve_laws(&self, material_id: u64) -> Vec<LeyKind> {
1478        let mut out = Vec::new();
1479        let mut cur = Some(material_id);
1480        let mut depth = 0;
1481        while let Some(cid) = cur {
1482            if depth > 32 {
1483                break;
1484            }
1485            let Some(m) = self.material(cid) else { break };
1486            for u in &m.leyes {
1487                if let Some(ley) = self.ley(u.ley) {
1488                    let mut kind = ley.kind.clone();
1489                    for (i, &v) in u.params.iter().enumerate() {
1490                        kind.set_param(i, v);
1491                    }
1492                    out.push(kind);
1493                }
1494            }
1495            cur = m.parent;
1496            depth += 1;
1497        }
1498        out
1499    }
1500
1501    /// Si el material tiene una ley [`Fluir`](LeyKind::Fluir), sus parámetros
1502    /// `(gravedad, horizontal)` — lo que vuelve líquido a un material. `None` = no fluye.
1503    pub fn fluir_params(&self, material_id: u64) -> Option<(f32, f32)> {
1504        self.resolve_laws(material_id).into_iter().find_map(|k| match k {
1505            LeyKind::Fluir { gravedad, horizontal } => Some((gravedad, horizontal)),
1506            _ => None,
1507        })
1508    }
1509
1510    /// Si el material tiene una ley [`Crecer`](LeyKind::Crecer), su `velocidad`.
1511    /// `None` = no crece.
1512    pub fn crecer_velocidad(&self, material_id: u64) -> Option<f32> {
1513        self.resolve_laws(material_id).into_iter().find_map(|k| match k {
1514            LeyKind::Crecer { velocidad } => Some(velocidad),
1515            _ => None,
1516        })
1517    }
1518
1519    /// Parámetros de Fluir del **agua** (el material semilla `Water`), si los tiene.
1520    /// El studio lo usa para que el agua del bioma fluya según su ley.
1521    pub fn water_fluir(&self) -> Option<(f32, f32)> {
1522        self.fluir_params(self.material_id_for(Material::Water)?)
1523    }
1524
1525    /// Si el material tiene una ley [`Ecuacion`](LeyKind::Ecuacion) (con herencia), su
1526    /// `(campos, programa compilado, valores de parámetros)` — listo para correr con
1527    /// [`EcuacionSim`](crate::EcuacionSim) sobre las celdas del material en el preview.
1528    /// `None` si no tiene una, o si su fórmula no compila.
1529    pub fn ecuacion_de_material(
1530        &self,
1531        material_id: u64,
1532    ) -> Option<(Vec<FieldDef>, Program, Vec<f32>)> {
1533        self.resolve_laws(material_id).into_iter().find_map(|k| match &k {
1534            LeyKind::Ecuacion { campos, .. } => {
1535                let prog = k.compile_ecuacion()?.ok()?;
1536                Some((campos.clone(), prog, k.ecuacion_param_values()))
1537            }
1538            _ => None,
1539        })
1540    }
1541
1542    /// Arma la [`BiomaPalette`] resuelta de un bioma (colores concretos para el render).
1543    pub fn bioma_palette(&self, b: &Bioma) -> BiomaPalette {
1544        let agua = self
1545            .material_id_for(Material::Water)
1546            .map(|id| self.resolve_material(id).color)
1547            .unwrap_or_else(|| Material::Water.color());
1548        BiomaPalette {
1549            ground: self.resolve_material(b.ground),
1550            cliff: self.resolve_material(b.cliff),
1551            peak: b.peak.map(|id| self.resolve_material(id)),
1552            agua,
1553            objetos: b
1554                .objetos
1555                .iter()
1556                .map(|o| (self.resolve_material(o.material), o.densidad, o.forma))
1557                .collect(),
1558        }
1559    }
1560
1561    /// Todo lo que el render necesita de un mundo: su primer bioma (relieve), la
1562    /// semilla y la paleta resuelta. `None` si el mundo o su bioma no existen.
1563    pub fn render_mundo(&self, mundo_id: u64) -> Option<MundoRender> {
1564        let m = self.mundo(mundo_id)?;
1565        let bid = *m.biomas.first()?;
1566        let bioma = self.bioma(bid)?.clone();
1567        let palette = self.bioma_palette(&bioma);
1568        Some(MundoRender { bioma, seed: m.seed, palette })
1569    }
1570
1571    /// El render de un **bioma** suelto (modo Biomas), con una semilla fija de
1572    /// previsualización. `None` si el bioma no existe.
1573    pub fn render_bioma(&self, bioma_id: u64) -> Option<MundoRender> {
1574        let bioma = self.bioma(bioma_id)?.clone();
1575        let palette = self.bioma_palette(&bioma);
1576        Some(MundoRender { bioma, seed: 1337, palette })
1577    }
1578
1579    /// Ser `i` (por índice, como referencian los actores de una escena), o uno por
1580    /// defecto si el índice se sale.
1581    pub fn character_or_default(&self, i: usize) -> CharSpec {
1582        self.seres
1583            .get(i)
1584            .cloned()
1585            .unwrap_or_else(|| CharSpec::new("actor", Age::Adult))
1586    }
1587}
1588
1589#[cfg(test)]
1590mod tests {
1591    use super::*;
1592
1593    #[test]
1594    fn ley_ecuacion_compila_y_corre() {
1595        use crate::ecuacion::FieldEngine;
1596        // Autoría de una ley Gray–Scott con los tipos del studio (como haría la UI).
1597        let ley = LeyKind::Ecuacion {
1598            campos: vec![
1599                FieldDef::new("u", 1.0, 0.0, 1.0),
1600                FieldDef::new("v", 0.0, 0.0, 1.0),
1601            ],
1602            params: vec![
1603                ParamDef::new("Du", 0.16, 0.0, 1.0),
1604                ParamDef::new("Dv", 0.08, 0.0, 1.0),
1605                ParamDef::new("F", 0.06, 0.0, 0.1),
1606                ParamDef::new("k", 0.062, 0.0, 0.1),
1607            ],
1608            fuentes: vec![
1609                "Du * lap(u) - u * v * v + F * (1 - u)".into(),
1610                "Dv * lap(v) + u * v * v - (F + k) * v".into(),
1611            ],
1612        };
1613        let prog = ley.compile_ecuacion().expect("es Ecuacion").expect("compila");
1614        let LeyKind::Ecuacion { campos, .. } = &ley else { unreachable!() };
1615        let mut eng = FieldEngine::new([32, 1, 32], campos.clone());
1616        for z in 14..18 {
1617            for x in 14..18 {
1618                eng.set(1, x, 0, z, 0.5);
1619                eng.set(0, x, 0, z, 0.25);
1620            }
1621        }
1622        let params = ley.ecuacion_param_values();
1623        for _ in 0..300 {
1624            eng.step(&prog, &params, 1.0);
1625        }
1626        let (mut lo, mut hi) = (f32::INFINITY, f32::NEG_INFINITY);
1627        for z in 0..32 {
1628            for x in 0..32 {
1629                let v = eng.get(1, x, 0, z);
1630                lo = lo.min(v);
1631                hi = hi.max(v);
1632            }
1633        }
1634        assert!(hi - lo > 0.05, "la ley autorada formó patrón (rango={})", hi - lo);
1635    }
1636
1637    #[test]
1638    fn ley_ecuacion_error_de_parseo_ubicado() {
1639        let ley = LeyKind::Ecuacion {
1640            campos: vec![FieldDef::new("c", 0.0, 0.0, 1.0)],
1641            params: vec![],
1642            fuentes: vec!["k * lap(c)".into()], // `k` no declarado
1643        };
1644        let err = ley.compile_ecuacion().unwrap().unwrap_err();
1645        assert!(err.contains("campo «c»"), "ubica el campo: {err}");
1646        assert!(err.contains('k'), "menciona el símbolo desconocido: {err}");
1647    }
1648
1649    #[test]
1650    fn proyecto_round_trip_ron() {
1651        let p = Project::starter();
1652        let s = ron::ser::to_string(&p).expect("serializa a ron");
1653        let back: Project = ron::from_str(&s).expect("deserializa de ron");
1654        assert_eq!(back.biomas.len(), p.biomas.len());
1655        assert_eq!(back.mundos.len(), p.mundos.len());
1656        assert_eq!(back.biomas[0].name, "desierto");
1657        // El relieve del bioma sobrevive el viaje (un parámetro de muestra).
1658        assert!((back.biomas[0].base - p.biomas[0].base).abs() < 1e-6);
1659        // Y los ids siguen apuntando: la escena referencia un mundo que existe.
1660        assert!(back.mundo(back.escenas[0].mundo).is_some());
1661    }
1662
1663    #[test]
1664    fn mundo_desde_mapa_persiste_en_ron() {
1665        let mut p = Project::starter();
1666        // Adjuntá una fuente de mapa a un mundo (como al importar en el studio).
1667        let mid = p.mundos[0].id;
1668        p.mundos[0].mapa = Some(MapaFuente {
1669            geojson: r#"{"type":"FeatureCollection","features":[]}"#.into(),
1670            pgm: Some(vec![1, 2, 3, 4]),
1671            dem_tif: None,
1672            rango_dem_m: 250.0,
1673        });
1674        let s = ron::ser::to_string(&p).expect("serializa a ron");
1675        let back: Project = ron::from_str(&s).expect("deserializa de ron");
1676        let m = back.mundo(mid).expect("el mundo sigue ahí");
1677        let fuente = m.mapa.as_ref().expect("la fuente del mapa sobrevive el viaje");
1678        assert!(fuente.geojson.contains("FeatureCollection"));
1679        assert_eq!(fuente.pgm.as_deref(), Some(&[1, 2, 3, 4][..]));
1680        assert!((fuente.rango_dem_m - 250.0).abs() < 1e-6);
1681    }
1682
1683    #[test]
1684    fn ron_viejo_sin_mapa_carga_como_procedural() {
1685        // Un mundo serializado ANTES de que existiera el campo `mapa` debe abrir
1686        // con `mapa: None` (compat hacia atrás vía `#[serde(default)]`).
1687        let ron_viejo = r#"(id:7,name:"viejo",seed:1,biomas:[])"#;
1688        let m: Mundo = ron::from_str(ron_viejo).expect("mundo legacy deserializa");
1689        assert!(m.mapa.is_none(), "sin campo mapa = procedural");
1690    }
1691
1692    #[test]
1693    fn herencia_de_material_overridea_solo_lo_redefinido() {
1694        let mut p = Project::starter();
1695        // «cactus amarillo» hijo de «cactus»: sólo redefine el color.
1696        let cactus = p.material_id_for(Material::Cactus).unwrap();
1697        let verde = p.resolve_material(cactus);
1698        let hijo_id = p.alloc_id();
1699        p.materiales.push(MaterialDef {
1700            id: hijo_id,
1701            name: "cactus amarillo".into(),
1702            parent: Some(cactus),
1703            role: MatRole::Objeto(Forma::Columnar),
1704            color: Some([0.9, 0.85, 0.2]),
1705            grain: None, // hereda
1706            brillo: None,
1707            rugosidad: None,
1708            bisel: None,
1709            leyes: vec![],
1710            builtin: None,
1711        });
1712        let hijo = p.resolve_material(hijo_id);
1713        assert_ne!(hijo.color, verde.color, "el color se redefinió");
1714        assert_eq!(hijo.grain, verde.grain, "el grano se heredó del padre");
1715    }
1716
1717    #[test]
1718    fn agua_fluye_por_su_ley_y_hereda_leyes() {
1719        let p = Project::starter();
1720        // El agua semilla tiene la ley Fluir con sus params.
1721        let (g, h) = p.water_fluir().expect("el agua tiene ley Fluir");
1722        assert!(g > 0.0 && h >= 0.0);
1723        // Un material sin leyes no fluye.
1724        let roca = p.material_id_for(Material::Rock).unwrap();
1725        assert!(p.fluir_params(roca).is_none());
1726        // El cactus crece (ley Crecer en el starter).
1727        let cactus = p.material_id_for(Material::Cactus).unwrap();
1728        assert!(p.crecer_velocidad(cactus).is_some());
1729
1730        // Herencia de leyes: un hijo del agua hereda Fluir aunque no la redefina.
1731        let mut p2 = p.clone();
1732        let agua = p2.material_id_for(Material::Water).unwrap();
1733        let hijo = p2.alloc_id();
1734        p2.materiales.push(MaterialDef {
1735            id: hijo,
1736            name: "agua turbia".into(),
1737            parent: Some(agua),
1738            role: MatRole::Terreno,
1739            color: Some([0.2, 0.4, 0.5]),
1740            grain: None,
1741            brillo: None,
1742            rugosidad: None,
1743            bisel: None,
1744            leyes: vec![],
1745            builtin: None,
1746        });
1747        assert!(p2.fluir_params(hijo).is_some(), "el hijo hereda Fluir del padre");
1748    }
1749
1750    #[test]
1751    fn detalle_sub_voxel_hereda_y_overridea() {
1752        let mut p = Project::starter();
1753        // Default = liso/duro (0,0) para un material sin detalle declarado.
1754        let roca = p.material_id_for(Material::Rock).unwrap();
1755        assert_eq!(p.detalle_material(roca), (0.0, 0.0), "default = sin detalle");
1756        // Padre con rugosidad; hijo sin nada → la hereda. Nieto redefine bisel.
1757        let padre = p.alloc_id();
1758        p.materiales.push(MaterialDef {
1759            id: padre,
1760            name: "roca rugosa".into(),
1761            parent: None,
1762            role: MatRole::Terreno,
1763            color: None,
1764            grain: None,
1765            brillo: None,
1766            rugosidad: Some(0.9),
1767            bisel: None,
1768            leyes: vec![],
1769            builtin: None,
1770        });
1771        let hijo = p.alloc_id();
1772        p.materiales.push(MaterialDef {
1773            id: hijo,
1774            name: "hijo".into(),
1775            parent: Some(padre),
1776            role: MatRole::Terreno,
1777            color: None,
1778            grain: None,
1779            brillo: None,
1780            rugosidad: None,
1781            bisel: Some(0.7),
1782            leyes: vec![],
1783            builtin: None,
1784        });
1785        let (rug, bis) = p.detalle_material(hijo);
1786        assert!((rug - 0.9).abs() < 1e-6, "rugosidad heredada del padre");
1787        assert!((bis - 0.7).abs() < 1e-6, "bisel propio gana");
1788    }
1789
1790    #[test]
1791    fn detalle_por_voxel_se_hornea_por_material() {
1792        // Cierra la limitación HONESTA: dos materiales del bioma deben lucir distinto a
1793        // la vez → cada uno hornea su detalle POR VOXEL en el grid, vía el pipeline real
1794        // (resolve_material → bioma_palette → generate_window → grid.detail_at).
1795        let mut p = Project::starter();
1796        let b0 = p.biomas[0].clone();
1797        let (g_id, c_id) = (b0.ground, b0.cliff);
1798        assert_ne!(g_id, c_id, "el bioma starter distingue suelo de acantilado");
1799
1800        // Suelo RUGOSO+duro (rug alto, bisel 0); acantilado LISO (rug 0, bisel 1).
1801        let set_det = |p: &mut Project, id: u64, r: f32, b: f32| {
1802            let m = p.materiales.iter_mut().find(|m| m.id == id).unwrap();
1803            m.rugosidad = Some(r);
1804            m.bisel = Some(b);
1805        };
1806        set_det(&mut p, g_id, 1.4, 0.0);
1807        set_det(&mut p, c_id, 0.0, 1.0);
1808
1809        // La paleta resuelta lleva el detalle POR MATERIAL (lo que el escalar global
1810        // no distinguía).
1811        let rg = p.resolve_material(g_id);
1812        let rc = p.resolve_material(c_id);
1813        assert_eq!(rg.detalle, (1.4, 0.0), "suelo rugoso/duro");
1814        assert_eq!(rc.detalle, (0.0, 1.0), "acantilado liso/biselado");
1815        assert_ne!(rg.detalle, rc.detalle, "dos materiales, dos looks");
1816
1817        // Y se hornea por voxel al generar la ventana: aparecen ambos códigos distintos
1818        // + aire sin autor (retrocompat). Byte de detalle = 1 + round(norm·254): el
1819        // suelo da rug alto (~238) y bisel piso (1); el acantilado, rug piso y bisel alto.
1820        let palette = p.bioma_palette(&b0);
1821        let grid = b0.generate_window(7, &palette, [40, 48, 40], [0, 0]);
1822        let (mut suelo, mut cliff, mut aire) = (false, false, false);
1823        let dim = grid.dim();
1824        for z in 0..dim[2] {
1825            for y in 0..dim[1] {
1826                for x in 0..dim[0] {
1827                    match grid.detail_at(x, y, z).unwrap() {
1828                        [r, b] if r >= 200 && b <= 5 => suelo = true,
1829                        [r, b] if r <= 5 && b >= 200 => cliff = true,
1830                        [0, 0] => aire = true,
1831                        _ => {}
1832                    }
1833                }
1834            }
1835        }
1836        assert!(suelo, "el suelo rugoso se horneó por voxel");
1837        assert!(cliff, "el acantilado liso/biselado se horneó por voxel (distinto del suelo)");
1838        assert!(aire, "quedan voxels sin autor (aire/agua) en look global — retrocompat");
1839    }
1840
1841    #[test]
1842    fn herencia_con_ciclo_no_cuelga() {
1843        let mut p = Project::starter();
1844        let a = p.alloc_id();
1845        let b = p.alloc_id();
1846        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 });
1847        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 });
1848        // No debe colgarse; cae al gris neutro.
1849        let r = p.resolve_material(a);
1850        assert_eq!(r.color, [150, 150, 150]);
1851    }
1852
1853    #[test]
1854    fn charspec_se_materializa_con_la_edad() {
1855        let spec = CharSpec::new("nene", Age::Baby);
1856        let actor = spec.to_actor(Vec3::ZERO, 0.0);
1857        assert_eq!(actor.age, Age::Baby);
1858    }
1859
1860    #[test]
1861    fn world_dim_minimo_48_de_alto() {
1862        assert_eq!(world_dim(64)[1], 48); // 64*0.4=25.6 → clamp a 48
1863        assert_eq!(world_dim(192)[1], 76);
1864    }
1865
1866    #[test]
1867    fn escena_round_trip_y_compila_a_guiones() {
1868        let dim = world_dim(128);
1869        let s = SceneSpec::walk_and_emote("demo", 0, 3, Clip::Wave, dim);
1870        // RON ida y vuelta.
1871        let txt = ron::ser::to_string(&s).expect("ron");
1872        let back: SceneSpec = ron::from_str(&txt).expect("de-ron");
1873        assert_eq!(back.actors.len(), 3);
1874        // Compila a guiones reproducibles: a mitad de la caminata el actor se movió.
1875        let scripts = back.scripts();
1876        let start = scripts[0].sample(0.0);
1877        let mid = scripts[0].sample(1.3);
1878        assert!(mid.gx > start.gx, "el actor avanza en X: {} → {}", start.gx, mid.gx);
1879    }
1880
1881    #[test]
1882    fn plano_vigente_corta_en_el_tiempo() {
1883        let dim = world_dim(128);
1884        let s = SceneSpec::walk_and_emote("demo", 0, 2, Clip::Wave, dim);
1885        // Arranca en establecedor; tras el giro (t≈3) pasa a primer plano.
1886        assert_eq!(s.active_shot(0.5), ShotKind::Establishing);
1887        assert_eq!(s.active_shot(3.5), ShotKind::CloseUp);
1888        // El plano resuelve una cámara que mira al centroide.
1889        let look = Vec3::new(10.0, 2.0, 10.0);
1890        let cam = ShotKind::CloseUp.resolve(look, 9.0, 1.0);
1891        assert_eq!(cam.target, look);
1892        assert!((cam.eye - look).length() > 1.0, "el ojo está separado del objetivo");
1893    }
1894
1895    #[test]
1896    fn beats_son_cortes_y_gestos() {
1897        let dim = world_dim(128);
1898        let s = SceneSpec::walk_and_emote("demo", 0, 2, Clip::Wave, dim);
1899        // walk_and_emote: corte de cámara a t_turn (3.0) + el gesto Wave a t_turn.
1900        // Caen en el mismo instante → se funden en un solo beat.
1901        let beats = s.beat_times();
1902        assert!(!beats.is_empty(), "hay al menos un acento");
1903        assert!(beats.iter().all(|&t| t >= 0.0 && t <= s.duration + 0.1));
1904        assert!(beats.iter().any(|&t| (t - 3.0).abs() < 0.2), "acento cerca del gesto/corte");
1905    }
1906
1907    #[test]
1908    fn camara_en_mano_es_determinista_y_apagable() {
1909        // amt=0 → trípode: sin offset, exactamente cero.
1910        let (e0, t0) = handheld_shake(1.234, 0.0, 30.0);
1911        assert_eq!(e0, Vec3::ZERO);
1912        assert_eq!(t0, Vec3::ZERO);
1913
1914        // amt>0 → tiembla (offset no nulo) y es función pura de t (reproducible).
1915        let (e1, _) = handheld_shake(1.234, 0.7, 30.0);
1916        let (e2, _) = handheld_shake(1.234, 0.7, 30.0);
1917        assert_eq!(e1, e2, "mismo t → mismo temblor (peli reproducible)");
1918        assert!(e1.length() > 0.0, "con intensidad la cámara se mueve");
1919        // Instantes distintos → temblor distinto (no está congelado).
1920        let (e3, _) = handheld_shake(1.235, 0.7, 30.0);
1921        assert!((e1 - e3).length() > 0.0, "el temblor evoluciona en el tiempo");
1922    }
1923
1924    #[test]
1925    fn frame_rate_del_heroe_viaja_al_guion() {
1926        let dim = world_dim(128);
1927        let s = SceneSpec::walk_and_emote("demo", 0, 3, Clip::Wave, dim);
1928        // El primer actor (Héroe) anima en doses; los demás, fluidos.
1929        assert_eq!(s.actors[0].frame_rate, Some(12));
1930        assert_eq!(s.actors[1].frame_rate, None);
1931        // Y sobrevive la compilación a guion.
1932        assert_eq!(s.scripts()[0].frame_rate(), Some(12));
1933        // Cámara en mano por defecto encendida.
1934        assert!(s.handheld > 0.0);
1935    }
1936
1937    #[test]
1938    fn ventana_de_escena_sigue_al_reparto() {
1939        let dim = world_dim(128); // [128, 51, 128] → medio = 64
1940        // Un actor que camina lejos del origen (más allá de la caja finita vieja):
1941        // un keyframe en grilla 18, otro en 600 → coords de MUNDO, no acotadas.
1942        let s = SceneSpec {
1943            id: 0,
1944            name: "caminata larga".into(),
1945            mundo: 0,
1946            duration: 10.0,
1947            actors: vec![ActorSpec {
1948                character: 0,
1949                keys: vec![
1950                    ActorKeySpec { t: 0.0, gx: 18.0, gz: 64.0, clip: None, face: None },
1951                    ActorKeySpec { t: 10.0, gx: 600.0, gz: 64.0, clip: None, face: None },
1952                ],
1953                frame_rate: None,
1954            }],
1955            shots: vec![],
1956            handheld: 0.0,
1957            atmosfera: None,
1958            caida: None,
1959            sucesos: Vec::new(),
1960        };
1961        let scripts = s.scripts();
1962
1963        // Al final el reparto está cerca de wx=600: la ventana lo centra (su columna
1964        // local cae cerca de dim/2), cosa imposible en una caja fija centrada en 0.
1965        let o_fin = window_origin_for_cast(&scripts, 10.0, dim);
1966        let local_x = 600 - o_fin[0];
1967        assert!(
1968            (local_x - dim[0] as i32 / 2).abs() <= SCENE_WINDOW_STEP,
1969            "el reparto queda centrado en la ventana (local_x={local_x})"
1970        );
1971
1972        // El origen se snappea al paso (sólo salta a múltiplos) → no regenera por cuadro.
1973        assert_eq!(o_fin[0] % SCENE_WINDOW_STEP, 0);
1974        assert_eq!(o_fin[1] % SCENE_WINDOW_STEP, 0);
1975
1976        // Y la ventana se MUEVE entre el principio y el final (mundo infinito, no caja).
1977        let o_ini = window_origin_for_cast(&scripts, 0.0, dim);
1978        assert_ne!(o_ini[0], o_fin[0], "la ventana siguió al reparto en X");
1979
1980        // Sin reparto: mundo centrado en el origen (compatibilidad con el preview fijo).
1981        assert_eq!(window_origin_for_cast(&[], 0.0, dim), [0, 0]);
1982    }
1983
1984    #[test]
1985    fn starter_trae_escena_y_personajes() {
1986        let p = Project::starter();
1987        assert_eq!(p.seres.len(), 3);
1988        assert_eq!(p.escenas.len(), 1);
1989        // La escena referencia un mundo válido por id.
1990        assert!(p.mundo(p.escenas[0].mundo).is_some());
1991        // Los 30 materiales semilla están sembrados (flora + terreno + casa).
1992        assert_eq!(p.materiales.len(), 31);
1993        assert!(p.material_id_for(Material::Sand).is_some());
1994        assert!(p.material_id_for(Material::Basalto).is_some());
1995        assert!(p.material_id_for(Material::Ladrillo).is_some());
1996        assert!(p.material_id_for(Material::Vidrio).is_some());
1997        // El vidrio (y las rocas pulidas) resuelven brillo > 0; el ladrillo, mate.
1998        assert!(p.resolve_material(p.material_id_for(Material::Vidrio).unwrap()).brillo > 0.5);
1999        assert_eq!(p.resolve_material(p.material_id_for(Material::Ladrillo).unwrap()).brillo, 0.0);
2000    }
2001}