tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8
llimphi-voxel/src/studio.rs
Historia
Quién tocó qué
crudo
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, ¶ms, 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}