use super::*; /// Crea un item nuevo en el nivel activo y devuelve su id. pub(crate) fn new_item(model: &mut Model) -> u64 { let id = model.project.alloc_id(); match model.level { Level::Leyes => model.project.leyes.push(llimphi_voxel::Ley { id, name: format!("ley {id}"), kind: LeyKind::Fluir { gravedad: 1.0, horizontal: 0.6 }, }), Level::Materiales => model.project.materiales.push(MaterialDef { id, name: format!("material {id}"), parent: None, role: MatRole::Terreno, color: Some([0.6, 0.6, 0.6]), grain: Some(0.4), brillo: None, rugosidad: None, bisel: None, leyes: vec![], builtin: None, }), Level::Seres => { let mut c = CharSpec::new(format!("ser {id}"), Age::Adult); c.id = id; model.project.seres.push(c); } Level::Biomas => model.project.biomas.push(default_bioma(model, id)), Level::Mundos => { let bioma = model.project.biomas.first().map(|b| b.id).unwrap_or(0); model.project.mundos.push(Mundo { id, name: format!("mundo {id}"), seed: 1, biomas: vec![bioma], mapa: None }); } Level::Escenas => { let mundo = model.project.mundos.first().map(|m| m.id).unwrap_or(0); let dim = world_dim(PREVIEW_DIM_XZ); let mut s = SceneSpec::walk_and_emote(format!("escena {id}"), mundo, 3, Clip::Wave, dim); s.id = id; model.project.escenas.push(s); } Level::Interiores => model.project.interiores.push(Interior::nuevo(id, format!("sala {id}"))), } id } /// Un bioma por defecto que referencia los materiales del proyecto (o 0). pub(crate) fn default_bioma(model: &Model, id: u64) -> Bioma { let ground = model.project.material_id_for(Material::Grass).unwrap_or(0); let cliff = model.project.material_id_for(Material::Rock).unwrap_or(0); Bioma { id, name: format!("bioma {id}"), base: 0.22, dune: 0.10, relief: 0.6, mountains: 0.4, water_level: 0.30, rivers: 0.2, peak_at: 0.8, ground, cliff, peak: None, objetos: vec![], minerales: vec![], densidad_urbana: 0.4, casas: vec![], fauna: vec![], seres: vec![], } } /// Duplica el item seleccionado (copia con id nuevo) y devuelve su id. pub(crate) fn duplicate_item(model: &mut Model) -> Option { let sel = model.selected(); let id = model.project.alloc_id(); match model.level { Level::Leyes => { let mut x = model.project.ley(sel)?.clone(); x.id = id; x.name = format!("{} copia", x.name); model.project.leyes.push(x); } Level::Materiales => { let mut x = model.project.material(sel)?.clone(); x.id = id; x.name = format!("{} copia", x.name); x.builtin = None; // una copia editable, no la de fábrica model.project.materiales.push(x); } Level::Seres => { let mut x = sel_sere(model)?.clone(); x.id = id; x.name = format!("{} copia", x.name); model.project.seres.push(x); } Level::Biomas => { let mut x = model.project.bioma(sel)?.clone(); x.id = id; x.name = format!("{} copia", x.name); model.project.biomas.push(x); } Level::Mundos => { let mut x = model.project.mundo(sel)?.clone(); x.id = id; x.name = format!("{} copia", x.name); model.project.mundos.push(x); } Level::Escenas => { let mut x = sel_scene(model)?.clone(); x.id = id; x.name = format!("{} copia", x.name); model.project.escenas.push(x); } Level::Interiores => { let mut x = model.project.interiores.iter().find(|i| i.id == model.selected())?.clone(); x.id = id; x.name = format!("{} copia", x.name); model.project.interiores.push(x); } } Some(id) } /// Borra el item seleccionado del nivel activo. pub(crate) fn delete_selected(model: &mut Model) { let sel = model.selected(); match model.level { Level::Leyes => model.project.leyes.retain(|x| x.id != sel), Level::Materiales => model.project.materiales.retain(|x| x.id != sel), Level::Seres => model.project.seres.retain(|x| x.id != sel), Level::Biomas => model.project.biomas.retain(|x| x.id != sel), Level::Mundos => model.project.mundos.retain(|x| x.id != sel), Level::Escenas => model.project.escenas.retain(|x| x.id != sel), Level::Interiores => model.project.interiores.retain(|x| x.id != sel), } } /// Fija el nombre del item seleccionado. pub(crate) fn set_selected_name(model: &mut Model, name: String) { let sel = model.selected(); match model.level { Level::Leyes => { if let Some(x) = model.project.leyes.iter_mut().find(|x| x.id == sel) { x.name = name; } } Level::Materiales => { if let Some(x) = model.project.materiales.iter_mut().find(|x| x.id == sel) { x.name = name; } } Level::Seres => { if let Some(x) = model.project.seres.iter_mut().find(|x| x.id == sel) { x.name = name; } } Level::Biomas => { if let Some(x) = model.project.biomas.iter_mut().find(|x| x.id == sel) { x.name = name; } } Level::Mundos => { if let Some(x) = model.project.mundos.iter_mut().find(|x| x.id == sel) { x.name = name; } } Level::Escenas => { if let Some(x) = model.project.escenas.iter_mut().find(|x| x.id == sel) { x.name = name; } } Level::Interiores => { if let Some(x) = model.project.interiores.iter_mut().find(|x| x.id == sel) { x.name = name; } } } } /// Recarga `name_input`/`seed_input` desde el item seleccionado. pub(crate) fn sync_inputs(model: &mut Model) { model.name_input.set_text(&model.selected_name()); if model.level == Level::Mundos { let s = model.project.mundo(model.selected()).map(|m| m.seed).unwrap_or(0); model.seed_input.set_text(&s.to_string()); } } /// Reselecciona el primer item de cada nivel (tras cargar un proyecto). pub(crate) fn reselect_all(model: &mut Model) { model.sel[0] = model.project.leyes.first().map(|x| x.id).unwrap_or(0); model.sel[1] = model.project.materiales.first().map(|x| x.id).unwrap_or(0); model.sel[2] = model.project.seres.first().map(|x| x.id).unwrap_or(0); model.sel[3] = model.project.biomas.first().map(|x| x.id).unwrap_or(0); model.sel[4] = model.project.mundos.first().map(|x| x.id).unwrap_or(0); model.sel[5] = model.project.escenas.first().map(|x| x.id).unwrap_or(0); model.sel[6] = model.project.interiores.first().map(|x| x.id).unwrap_or(0); refrescar_mapa(model); // el mundo recién seleccionado puede venir de un mapa } /// Regenera el **grid cache** (`model.mapa`) desde la [`MapaFuente`] persistida en el /// mundo seleccionado, o lo limpia si ese mundo es procedural. Barato cuando no hay /// mapa (sólo asigna `None`). Se llama al abrir un proyecto (vía [`reselect_all`]), /// al cambiar de mundo y al importar/quitar un mapa. pub(crate) fn refrescar_mapa(model: &mut Model) { let mid = model.sel[Level::Mundos.index()]; let fuente = model.project.mundos.iter().find(|m| m.id == mid).and_then(|m| m.mapa.clone()); match fuente { Some(f) => { let dim = world_dim(PREVIEW_DIM_XZ); match mapa_import::generar(&f, dim) { Ok((grid, resumen)) => { model.mapa = Some(Arc::new(grid)); model.status = resumen; } Err(e) => { model.mapa = None; model.status = format!("mapa: {e}"); } } } None => model.mapa = None, } } /// Nombre legible de un ser a partir de la ruta de su foto (el nombre de archivo sin /// extensión), con un respaldo si la ruta viene vacía. pub(crate) fn nombre_desde_path(path: &str) -> String { std::path::Path::new(path) .file_stem() .and_then(|s| s.to_str()) .filter(|s| !s.is_empty()) .unwrap_or("retrato") .to_string() } // --- Accesores al item seleccionado (mut e inmut) --- pub(crate) fn sel_material_mut(model: &mut Model) -> Option<&mut MaterialDef> { let id = model.sel[1]; model.project.materiales.iter_mut().find(|x| x.id == id) } pub(crate) fn sel_ley(model: &Model) -> Option<&llimphi_voxel::Ley> { model.project.ley(model.sel[0]) } pub(crate) fn sel_ley_mut(model: &mut Model) -> Option<&mut llimphi_voxel::Ley> { let id = model.sel[0]; model.project.leyes.iter_mut().find(|x| x.id == id) } /// Catálogo de **leyes de fábrica** por ecuación (autoradas, editables). Ciclar /// reemplaza la `Ecuacion` actual por la siguiente del catálogo. Todas verificadas /// como comportamientos vivos por los tests de `llimphi-voxel`. pub(crate) fn law_presets() -> Vec<(&'static str, LeyKind)> { vec![ ("difusión", LeyKind::ecuacion_default()), ( "reacción-difusión", LeyKind::Ecuacion { campos: vec![ FieldDef::new("u", 1.0, 0.0, 1.0), FieldDef::new("v", 0.0, 0.0, 1.0), ], params: vec![ ParamDef::new("Du", 0.16, 0.0, 1.0), ParamDef::new("Dv", 0.08, 0.0, 1.0), ParamDef::new("F", 0.06, 0.0, 0.1), ParamDef::new("k", 0.062, 0.0, 0.1), ], fuentes: vec![ "Du * lap(u) - u * v * v + F * (1 - u)".into(), "Dv * lap(v) + u * v * v - (F + k) * v".into(), ], }, ), ( "calor", LeyKind::Ecuacion { campos: vec![FieldDef::new("t", 0.0, 0.0, 1.0)], params: vec![ParamDef::new("difus", 0.2, 0.0, 0.25)], fuentes: vec!["difus * lap(t)".into()], }, ), ( "crecer", LeyKind::Ecuacion { campos: vec![FieldDef::new("h", 0.0, 0.0, 1.0)], params: vec![ParamDef::new("vel", 0.05, 0.0, 0.3), ParamDef::new("tope", 0.8, 0.0, 1.0)], fuentes: vec!["vel * (h < tope)".into()], }, ), ] } /// La siguiente ley del catálogo tras la actual (por label del preset actual). pub(crate) fn next_preset(actual: &LeyKind) -> LeyKind { let presets = law_presets(); // Encuentra el preset cuya estructura de campos coincide con la actual; si no, // arranca del primero. Ciclo simple por índice. let idx = presets .iter() .position(|(_, k)| leyes_misma_forma(k, actual)) .map(|i| (i + 1) % presets.len()) .unwrap_or(0); presets[idx].1.clone() } /// Dos leyes `Ecuacion` "tienen la misma forma" si coinciden los nombres de campos /// (heurística para ubicar el preset actual al ciclar). pub(crate) fn leyes_misma_forma(a: &LeyKind, b: &LeyKind) -> bool { match (a, b) { (LeyKind::Ecuacion { campos: ca, .. }, LeyKind::Ecuacion { campos: cb, .. }) => { ca.len() == cb.len() && ca.iter().zip(cb).all(|(x, y)| x.name == y.name) } _ => false, } } pub(crate) fn sel_sere<'a>(model: &'a Model) -> Option<&'a CharSpec> { let id = model.sel[2]; model.project.seres.iter().find(|x| x.id == id) } pub(crate) fn sel_sere_mut(model: &mut Model) -> Option<&mut CharSpec> { let id = model.sel[2]; model.project.seres.iter_mut().find(|x| x.id == id) } pub(crate) fn sel_bioma_mut(model: &mut Model) -> Option<&mut Bioma> { let id = model.sel[3]; model.project.biomas.iter_mut().find(|x| x.id == id) } pub(crate) fn sel_interior_mut(model: &mut Model) -> Option<&mut Interior> { let id = model.sel[6]; model.project.interiores.iter_mut().find(|x| x.id == id) } pub(crate) fn sel_mundo_mut(model: &mut Model) -> Option<&mut Mundo> { let id = model.sel[4]; model.project.mundos.iter_mut().find(|x| x.id == id) } /// Mapea el teclado a mensajes de JUEGO (movimiento en primera persona). /// WASD mueve, E/Q suben/bajan en vuelo, Espacio salta (a pie), F alterna vuelo, /// R nueva semilla, Esc sale. pub(crate) fn juego_on_key(ev: &KeyEvent) -> Option { let pressed = matches!(ev.state, KeyState::Pressed); match &ev.key { Key::Named(NamedKey::Escape) if pressed && !ev.repeat => Some(Msg::EntrarSalir), Key::Named(NamedKey::Space) => Some(Msg::Saltar(pressed)), Key::Character(c) => match c.to_ascii_lowercase().as_str() { "w" => Some(Msg::MoverJuego(EjeJuego::Fwd, pressed)), "s" => Some(Msg::MoverJuego(EjeJuego::Back, pressed)), "a" => Some(Msg::MoverJuego(EjeJuego::Left, pressed)), "d" => Some(Msg::MoverJuego(EjeJuego::Right, pressed)), "e" => Some(Msg::MoverJuego(EjeJuego::Arriba, pressed)), "q" => Some(Msg::MoverJuego(EjeJuego::Abajo, pressed)), "f" if pressed && !ev.repeat => Some(Msg::AlternarVuelo), "r" if pressed && !ev.repeat => Some(Msg::RerollSemilla), // Dictar sucesos donde estás parado: 1 aparece un ser, 2 marca de // cámara, 3 punto de interés, 4 acá se desmorona el mundo. "1" if pressed && !ev.repeat => Some(Msg::DictarSuceso(TipoSuceso::Aparece(0))), "2" if pressed && !ev.repeat => Some(Msg::DictarSuceso(TipoSuceso::Camara)), "3" if pressed && !ev.repeat => Some(Msg::DictarSuceso(TipoSuceso::Marca)), "4" if pressed && !ev.repeat => { Some(Msg::DictarSuceso(TipoSuceso::Desmorona { radio: 4.0, motas: 32 })) } _ => None, }, _ => None, } } pub(crate) fn sel_scene<'a>(model: &'a Model) -> Option<&'a SceneSpec> { let id = model.sel[5]; model.project.escenas.iter().find(|x| x.id == id) } pub(crate) fn sel_scene_mut(model: &mut Model) -> Option<&mut SceneSpec> { let id = model.sel[5]; model.project.escenas.iter_mut().find(|x| x.id == id) } // --- Listas de ids para los botones de ciclo --- pub(crate) fn terreno_ids(model: &Model) -> Vec { model.project.materiales.iter().map(|m| m.id).collect() } pub(crate) fn objeto_ids(model: &Model) -> Vec { let objs: Vec = model .project .materiales .iter() .filter(|m| matches!(m.role, MatRole::Objeto(_))) .map(|m| m.id) .collect(); if objs.is_empty() { terreno_ids(model) } else { objs } } pub(crate) fn bioma_ids(model: &Model) -> Vec { model.project.biomas.iter().map(|b| b.id).collect() } pub(crate) fn sere_ids(model: &Model) -> Vec { model.project.seres.iter().map(|s| s.id).collect() } /// Reparte los tiempos de las claves de un actor uniformemente en `[0, dur]`, así el /// recorrido se recorre a lo largo de toda la escena (el director interpola entre ellas). pub(crate) fn respread(actor: &mut ActorSpec, dur: f32) { let n = actor.keys.len(); for (i, k) in actor.keys.iter_mut().enumerate() { k.t = if n <= 1 { 0.0 } else { i as f32 / (n - 1) as f32 * dur }; } } pub(crate) fn mundo_ids(model: &Model) -> Vec { model.project.mundos.iter().map(|m| m.id).collect() } /// El siguiente id de `ids` después de `cur` (cicla); `cur` si la lista está vacía. pub(crate) fn next_in(ids: &[u64], cur: u64) -> u64 { if ids.is_empty() { return cur; } let i = ids.iter().position(|&x| x == cur).unwrap_or(usize::MAX); ids[(i.wrapping_add(1)) % ids.len()] } /// Cicla un `Option` por `None → ids[0] → … → None`. pub(crate) fn next_option(cur: Option, ids: &[u64]) -> Option { match cur { None => ids.first().copied(), Some(c) => { let i = ids.iter().position(|&x| x == c); match i { Some(i) if i + 1 < ids.len() => Some(ids[i + 1]), _ => None, } } } } // ============================================================================= // Helpers de layout // =============================================================================