gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-voxel-studio/src/pruebas.rs Rust · 383 líneas · 17.9 KB Historia Quién tocó qué crudo

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

1use super::*;
2
3#[cfg(test)]
4mod tests {
5    use super::*;
6
7    /// El laboratorio corre una ley autorada del catálogo y forma estructura: el campo
8    /// visible desarrolla rango espacial (certifica símbolos + params + programa + step,
9    /// la cadena de app completa, sin GPU ni screenshot).
10    #[test]
11    fn lab_corre_ley_del_catalogo_y_forma_patron() {
12        let ley = law_presets().into_iter().find(|(n, _)| *n == "reacción-difusión").unwrap().1;
13        let LeyKind::Ecuacion { campos, .. } = &ley else { panic!("es Ecuacion") };
14        let prog = ley.compile_ecuacion().unwrap().expect("compila");
15        let mut lab = LawLab::build(1, 1, campos, Ok(prog), 1);
16        let params = ley.ecuacion_param_values();
17        for _ in 0..80 {
18            lab.step(&params); // 80 · LAB_SUBSTEPS pasos
19        }
20        let (mut lo, mut hi) = (f32::INFINITY, f32::NEG_INFINITY);
21        for z in 0..LAB_DIM {
22            for x in 0..LAB_DIM {
23                let v = lab.engine.get(1, x, 0, z);
24                lo = lo.min(v);
25                hi = hi.max(v);
26            }
27        }
28        assert!(hi - lo > 0.05, "la ley formó patrón en el lab (rango={})", hi - lo);
29        // El heatmap se construye a la resolución del lab sin panics.
30        let _img = heatmap_image(&lab);
31    }
32
33    /// Ciclar el catálogo desde la difusión por defecto entrega la reacción‑difusión.
34    #[test]
35    fn preset_cicla_al_siguiente() {
36        let n = next_preset(&LeyKind::ecuacion_default());
37        match &n {
38            LeyKind::Ecuacion { campos, .. } => assert_eq!(campos.len(), 2),
39            _ => panic!("sigue siendo Ecuacion"),
40        }
41    }
42
43    /// Cada rótulo del vestuario existe en los TRES catálogos. Una clave que falta no
44    /// da error: muestra el identificador crudo, y sólo en la máquina del que habla el
45    /// idioma que faltó. Recorre todos los peinados y sombreros por la MISMA función
46    /// que usa el editor, así una prenda nueva sin su clave se ve acá.
47    #[test]
48    fn el_vestuario_se_lee_en_los_tres_idiomas() {
49        let mut claves: Vec<String> = [
50            "voxelstudio-tab-wardrobe",
51            "voxelstudio-hair",
52            "voxelstudio-hat",
53            "voxelstudio-glasses-on",
54            "voxelstudio-glasses-off",
55            "voxelstudio-beard",
56            "voxelstudio-skirt",
57            "voxelstudio-cloak",
58            "voxelstudio-color-hat",
59            "voxelstudio-color-skirt",
60            "voxelstudio-color-cloak",
61            "voxelstudio-species",
62            "voxelstudio-species-none",
63        ]
64        .iter()
65        .map(|s| s.to_string())
66        .collect();
67        for p in Peinado::TODOS {
68            let a = llimphi_voxel::Atuendo::con(p);
69            claves.push(format!("voxelstudio-{}", clave_prenda(&a, "voxelstudio-hair")));
70        }
71        for s in Sombrero::TODOS {
72            let a = llimphi_voxel::Atuendo { sombrero: s, ..Default::default() };
73            claves.push(format!("voxelstudio-{}", clave_prenda(&a, "voxelstudio-hat")));
74        }
75        // Diez peinados y siete sombreros: ninguno comparte clave con otro.
76        let mut unicas = claves.clone();
77        unicas.sort();
78        unicas.dedup();
79        assert_eq!(unicas.len(), claves.len(), "dos prendas con la misma clave");
80        for idioma in ["es-PE", "en-US", "qu-PE"] {
81            rimay_localize::set_locale(idioma).unwrap();
82            for k in &claves {
83                let txt = rimay_localize::t(k);
84                assert_ne!(&txt, k, "{idioma}: falta {k}");
85                assert!(!txt.trim().is_empty(), "{idioma}: {k} vacía");
86            }
87        }
88    }
89
90    /// Las fórmulas de peso de los impulsos de fábrica parsean con la MISMA tabla de
91    /// símbolos que expone el editor de conducta — guarda contra que se desincronicen los
92    /// nombres de percepts/params entre el pie de ayuda y el parser. Y `materializar`
93    /// siembra los impulsos de fábrica en un ser que no tenía.
94    #[test]
95    fn conducta_impulsos_de_fabrica_coherentes_con_el_editor() {
96        let sym = llimphi_voxel::Conducta::symbols();
97        for imp in llimphi_voxel::default_impulsos() {
98            assert!(
99                llimphi_voxel::Expr::parse(&imp.peso, &sym).is_ok(),
100                "la fórmula de fábrica «{}» parsea con los símbolos del editor",
101                imp.peso
102            );
103        }
104        let mut c = CharSpec::new("x", Age::Adult);
105        c.conducta.impulsos.clear();
106        materializar_impulsos(&mut c);
107        assert_eq!(c.conducta.impulsos.len(), llimphi_voxel::default_impulsos().len());
108        // Ciclar la primitiva del primer impulso lo mueve a la siguiente.
109        let antes = c.conducta.impulsos[0].primitiva;
110        c.conducta.impulsos[0].primitiva = antes.next();
111        assert_ne!(antes, c.conducta.impulsos[0].primitiva);
112    }
113
114    /// **El editor de minerales funciona de punta a punta**: despachando los `Msg`
115    /// reales por `update()` (con un handle de test), cada factor del mineral cambia
116    /// en el proyecto y sube `gen` (lo que dispara la regeneración del preview). Es la
117    /// prueba headless de que arrastrar los sliders SÍ modifica el mundo.
118    #[test]
119    fn editar_mineral_recorre_update_y_cambia_el_bioma() {
120        let h = Handle::for_test();
121        let mut m = demo_model();
122        m.level = Level::Biomas;
123        m.tool_tab = 3;
124        let bid = m.project.biomas.first().map(|b| b.id).expect("hay biomas");
125        m.sel[3] = bid;
126        let g0 = m.gen;
127        let n_min = m.project.bioma(bid).unwrap().minerales.len();
128        assert!(n_min > 0, "el bioma de arranque trae minerales");
129
130        // Agregar un mineral por el mensaje real.
131        m = Studio::update(m, Msg::AddBiomaMineral, &h);
132        assert_eq!(m.project.bioma(bid).unwrap().minerales.len(), n_min + 1);
133
134        // Editar cada factor del último mineral por su `Msg`.
135        let i = n_min; // índice del recién agregado
136        let tipo0 = m.project.bioma(bid).unwrap().minerales[i].mineral;
137        m = Studio::update(m, Msg::CycleMineralTipo(i), &h);
138        assert_ne!(tipo0, m.project.bioma(bid).unwrap().minerales[i].mineral, "el tipo cambió");
139
140        let asp0 = m.project.bioma(bid).unwrap().minerales[i].aspecto;
141        m = Studio::update(m, Msg::CycleMineralAspecto(i), &h);
142        assert_ne!(asp0, m.project.bioma(bid).unwrap().minerales[i].aspecto, "el aspecto cambió");
143
144        m = Studio::update(m, Msg::SetMineralBrillo(i, 1.0), &h);
145        assert_eq!(m.project.bioma(bid).unwrap().minerales[i].brillo, 1.0, "brillo al máximo");
146
147        m = Studio::update(m, Msg::SetMineralDureza(i, 9.0), &h);
148        assert_eq!(m.project.bioma(bid).unwrap().minerales[i].dureza, 9.0, "dureza editada");
149
150        m = Studio::update(m, Msg::SetMineralColor(i, 0, 1.0), &h);
151        assert_eq!(m.project.bioma(bid).unwrap().minerales[i].color[0], 255, "color R al tope");
152
153        m = Studio::update(m, Msg::SetMineralAbundancia(i, 0.9), &h);
154        assert_eq!(m.project.bioma(bid).unwrap().minerales[i].abundancia, 0.9, "abundancia editada");
155
156        assert!(m.gen > g0, "cada edición subió gen → el preview se regenera");
157
158        // Y el tab Objetos: ciclar la forma de un objeto también viaja por update.
159        let bid_flora = m.project.biomas.iter().find(|b| !b.objetos.is_empty()).map(|b| b.id);
160        if let Some(fid) = bid_flora {
161            m.sel[3] = fid;
162            let f0 = m.project.bioma(fid).unwrap().objetos[0].forma;
163            m = Studio::update(m, Msg::CycleObjetoForma(0), &h);
164            assert_ne!(f0, m.project.bioma(fid).unwrap().objetos[0].forma, "la forma del objeto cambió");
165        }
166    }
167
168    /// **El editor de interiores coloca muebles**: despachando los `Msg` reales por
169    /// `update()`, cambiar el pincel y clickear el piso (PlaceMuebleAt) agrega una
170    /// pieza al interior seleccionado, quitar/vaciar las saca, y editar la sala sube
171    /// `gen`. Prueba headless de colocar-con-el-mouse (la proyección click→celda la
172    /// hace `pick_world`, que necesita GPU; aquí probamos el camino de datos).
173    #[test]
174    fn editor_interiores_coloca_y_edita() {
175        let h = Handle::for_test();
176        let mut m = demo_model();
177        m.level = Level::Interiores;
178        let iid = m.project.interiores.first().map(|i| i.id).expect("hay un interior");
179        m.sel[6] = iid;
180        let n0 = m.project.interiores.iter().find(|i| i.id == iid).unwrap().piezas.len();
181
182        // Elegir un mueble-pincel y orientación por los mensajes reales.
183        let brush0 = m.mueble_brush;
184        m = Studio::update(m, Msg::CycleMuebleBrush, &h);
185        assert_ne!(brush0, m.mueble_brush, "el pincel cambió");
186        m = Studio::update(m, Msg::CycleMuebleFacing, &h);
187        assert_eq!(m.mueble_facing, 1, "la orientación giró");
188
189        // Colocar sobre una celda del piso.
190        let g0 = m.gen;
191        m = Studio::update(m, Msg::PlaceMuebleAt(6.0, 5.0), &h);
192        let it = m.project.interiores.iter().find(|i| i.id == iid).unwrap();
193        assert_eq!(it.piezas.len(), n0 + 1, "se colocó una pieza");
194        assert_eq!(it.piezas.last().unwrap().mueble, m.mueble_brush);
195        assert!(m.gen > g0, "colocar regeneró el preview");
196
197        // Quitar la última y editar la sala.
198        m = Studio::update(m, Msg::RemoveLastPieza, &h);
199        assert_eq!(m.project.interiores.iter().find(|i| i.id == iid).unwrap().piezas.len(), n0);
200        m = Studio::update(m, Msg::SetInteriorAncho(30.0), &h);
201        assert_eq!(m.project.interiores.iter().find(|i| i.id == iid).unwrap().ancho, 30);
202        m = Studio::update(m, Msg::ToggleInteriorMuros, &h);
203        assert!(!m.project.interiores.iter().find(|i| i.id == iid).unwrap().muros);
204    }
205
206    /// La rampa cubre de oscuro a claro en los extremos (heatmap legible).
207    #[test]
208    fn rampa_va_de_oscuro_a_claro() {
209        let bajo = ramp(0.0);
210        let alto = ramp(1.0);
211        assert!(bajo.iter().map(|&c| c as u32).sum::<u32>() < 100, "extremo bajo oscuro");
212        assert!(alto[0] > 200 && alto[1] > 200, "extremo alto claro");
213    }
214
215    /// **El editor convierte un ser en elefante y lo devuelve a persona**, por los
216    /// `Msg` reales: hacia adelante llega al elefante, cada paso sube `gen` (lo que
217    /// refresca el preview), y hacia atrás desde el primero vuelve a «ninguna».
218    #[test]
219    fn el_editor_vuelve_elefante_a_un_ser_y_lo_devuelve() {
220        use llimphi_voxel::Bicho;
221        let h = Handle::for_test();
222        let mut m = demo_model();
223        m.level = Level::Seres;
224        m.sel[2] = m.project.seres[0].id;
225        assert_eq!(sel_sere(&m).unwrap().bicho, None);
226        let mut pasos = 0;
227        while sel_sere(&m).unwrap().bicho != Some(Bicho::Elefante) {
228            let g = m.gen;
229            m = Studio::update(m, Msg::CycleSereBicho(false), &h);
230            assert_eq!(m.gen, g + 1, "cambiar la especie refresca el preview");
231            pasos += 1;
232            assert!(pasos <= Bicho::ALL.len(), "el elefante no aparece ciclando");
233        }
234        assert!(sel_sere(&m).unwrap().es_sdf(), "el elefante se pinta como SDF");
235        while sel_sere(&m).unwrap().bicho.is_some() {
236            m = Studio::update(m, Msg::CycleSereBicho(true), &h);
237        }
238        assert!(sel_sere(&m).unwrap().bicho.is_none(), "hacia atrás vuelve a persona");
239    }
240
241    /// **La utilería se dirige desde el editor**, por los `Msg` reales: una pieza
242    /// nueva nace en la mano del primer actor, cambia de cosa, pasa al mundo, se
243    /// mueve y se quita. Y cada rótulo de la pestaña está en los tres idiomas.
244    #[test]
245    fn la_utileria_se_dirige_desde_el_editor_y_se_lee_en_los_tres_idiomas() {
246        let h = Handle::for_test();
247        let mut m = demo_model();
248        m.level = Level::Escenas;
249        let id = m.project.alloc_id();
250        let mut sc = SceneSpec::walk_and_emote("prendedor", 0, 2, Clip::Wave, [96, 24, 96]);
251        sc.id = id;
252        m.project.escenas.push(sc);
253        m.sel[5] = id;
254        m.time = 1.5;
255        m = Studio::update(m, Msg::AddUtileria, &h);
256        let u = &sel_scene(&m).unwrap().utileria[0];
257        assert_eq!((u.cosa, u.lleva, u.desde), (Utileria::Estrella, Some(0), 1.5));
258        m = Studio::update(m, Msg::CycleUtileriaCosa, &h);
259        assert_eq!(sel_scene(&m).unwrap().utileria[0].cosa, Utileria::Flor);
260        // Nadie → actor 0 → actor 1 → nadie: desde el actor 0, dos toques la sueltan.
261        m = Studio::update(m, Msg::CycleUtileriaLleva, &h);
262        assert_eq!(sel_scene(&m).unwrap().utileria[0].lleva, Some(1));
263        m = Studio::update(m, Msg::CycleUtileriaLleva, &h);
264        assert_eq!(sel_scene(&m).unwrap().utileria[0].lleva, None);
265        m = Studio::update(m, Msg::SetUtileria(UtileriaField::X, 33.0), &h);
266        m = Studio::update(m, Msg::SetUtileria(UtileriaField::Giro, 9.0), &h);
267        let u = &sel_scene(&m).unwrap().utileria[0];
268        assert_eq!(u.pos[0], 33.0);
269        assert_eq!(u.giro, 1.0, "el giro se acota");
270        // El editor se arma (con la pieza en el mundo, con sus sliders de lugar).
271        assert!(utileria_tools(&m, sel_scene(&m).unwrap()).len() > 8);
272        m = Studio::update(m, Msg::RemoveUtileria, &h);
273        assert!(sel_scene(&m).unwrap().utileria.is_empty());
274
275        let mut claves: Vec<&str> = vec![
276            "voxelstudio-tab-props",
277            "voxelstudio-prop-row",
278            "voxelstudio-prop-empty",
279            "voxelstudio-prop-add",
280            "voxelstudio-prop-remove",
281            "voxelstudio-prop-kind",
282            "voxelstudio-prop-held-by",
283            "voxelstudio-prop-held-nobody",
284            "voxelstudio-prop-x",
285            "voxelstudio-prop-y",
286            "voxelstudio-prop-z",
287            "voxelstudio-prop-size",
288            "voxelstudio-prop-from",
289            "voxelstudio-prop-spin",
290        ];
291        claves.extend(Utileria::ALL.iter().map(|&u| clave_utileria(u)));
292        for idioma in ["es-PE", "en-US", "qu-PE"] {
293            rimay_localize::set_locale(idioma).unwrap();
294            for k in &claves {
295                let txt = rimay_localize::t(k);
296                assert_ne!(&txt, k, "{idioma}: falta {k}");
297            }
298        }
299    }
300
301    /// **La noche se prende desde el editor**: el botón de cielo da vuelta día ↔
302    /// noche, las perillas se acotan, y los rótulos están en los tres idiomas.
303    #[test]
304    fn la_noche_se_prende_desde_el_editor_y_se_lee_en_los_tres_idiomas() {
305        let h = Handle::for_test();
306        let mut m = demo_model();
307        m.level = Level::Escenas;
308        let id = m.project.alloc_id();
309        let mut sc = SceneSpec::walk_and_emote("la estrella", 0, 1, Clip::Wave, [96, 24, 96]);
310        sc.id = id;
311        m.project.escenas.push(sc);
312        m.sel[5] = id;
313        assert!(sel_scene(&m).unwrap().cielo.is_none(), "una escena nace de día");
314        let de_dia = cielo_tools(&m, sel_scene(&m).unwrap()).len();
315        m = Studio::update(m, Msg::ToggleNoche, &h);
316        assert_eq!(sel_scene(&m).unwrap().cielo, Some(Cielo::noche()));
317        assert!(cielo_tools(&m, sel_scene(&m).unwrap()).len() > de_dia, "de noche aparecen las perillas");
318        m = Studio::update(m, Msg::SetCielo(CieloField::Estrellas, 3.0), &h);
319        m = Studio::update(m, Msg::SetCielo(CieloField::Luna, 0.09), &h);
320        let c = sel_scene(&m).unwrap().cielo.unwrap();
321        assert_eq!((c.estrellas, c.luna_radio), (1.0, 0.09), "las estrellas se acotan a 1");
322        m = Studio::update(m, Msg::ToggleNoche, &h);
323        assert!(sel_scene(&m).unwrap().cielo.is_none(), "y vuelve el día");
324
325        for idioma in ["es-PE", "en-US", "qu-PE"] {
326            rimay_localize::set_locale(idioma).unwrap();
327            for k in ["voxelstudio-sky-day", "voxelstudio-sky-night", "voxelstudio-sky-stars", "voxelstudio-sky-moon"] {
328                let txt = rimay_localize::t(k);
329                assert_ne!(&txt, k, "{idioma}: falta {k}");
330            }
331        }
332    }
333
334    /// **El decorado se arma desde el editor**: una construcción nueva nace como
335    /// palacio al fondo del recorrido, cambia de estilo, de variante y de lugar, y
336    /// se quita. Y cada rótulo —incluido el nombre de cada estilo de casa— está
337    /// en los tres idiomas.
338    #[test]
339    fn el_decorado_se_arma_desde_el_editor_y_se_lee_en_los_tres_idiomas() {
340        let h = Handle::for_test();
341        let mut m = demo_model();
342        m.level = Level::Escenas;
343        let id = m.project.alloc_id();
344        let mut sc = SceneSpec::walk_and_emote("el palacio", 0, 1, Clip::Wave, [96, 24, 96]);
345        sc.id = id;
346        let fin = *sc.actors[0].keys.last().unwrap();
347        m.project.escenas.push(sc);
348        m.sel[5] = id;
349        m = Studio::update(m, Msg::AddDecorado, &h);
350        let d = &sel_scene(&m).unwrap().decorado[0];
351        assert_eq!(d.casa, CasaTipo::Palacio);
352        assert_eq!(d.pos, [fin.gx, fin.gz + 18.0], "nace detrás del final del recorrido");
353        m = Studio::update(m, Msg::CycleDecoradoCasa, &h);
354        assert_ne!(sel_scene(&m).unwrap().decorado[0].casa, CasaTipo::Palacio);
355        m = Studio::update(m, Msg::OtraVarianteDecorado, &h);
356        assert_eq!(sel_scene(&m).unwrap().decorado[0].semilla, 1);
357        m = Studio::update(m, Msg::SetDecorado(DecoradoField::X, 40.4), &h);
358        assert_eq!(sel_scene(&m).unwrap().decorado[0].pos[0], 40.0, "se planta en una columna entera");
359        assert!(decorado_tools(&m, sel_scene(&m).unwrap()).len() > 6);
360        m = Studio::update(m, Msg::RemoveDecorado, &h);
361        assert!(sel_scene(&m).unwrap().decorado.is_empty());
362
363        let mut claves: Vec<&str> = vec![
364            "voxelstudio-tab-sets",
365            "voxelstudio-set-row",
366            "voxelstudio-set-empty",
367            "voxelstudio-set-add",
368            "voxelstudio-set-remove",
369            "voxelstudio-set-kind",
370            "voxelstudio-set-variant",
371            "voxelstudio-set-x",
372            "voxelstudio-set-z",
373        ];
374        claves.extend(CasaTipo::ALL.iter().map(|&c| clave_casa(c)));
375        for idioma in ["es-PE", "en-US", "qu-PE"] {
376            rimay_localize::set_locale(idioma).unwrap();
377            for k in &claves {
378                let txt = rimay_localize::t(k);
379                assert_ne!(&txt, k, "{idioma}: falta {k}");
380            }
381        }
382    }
383}