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