Desde hace algúnos días, he estado trabajando en una aplicación bastante interesante. Se llama Lore Designer, una aplicación de escritorio hecha para planificar el desarrollo de videojuegos. Es completamente Open Source y Local-First. Hasta el momento, solo dos personas conocían sobre el proyecto, pero supuse que ya era momento de hablar de ello en público.
Aún está muy lejos de estar lista, tiene un chingo de bugs y fallas, pero por razónes históricas (y para obligarme a seguir codificándolo), quería empezar un devlog para compartir detalles del progreso de la aplicación. Hay muchas cosas que me gustaría compartir y quizás a alguien le interesen (o quizás haya gente que quiera corregirme y eso me enseñaría algo)
Mi forma de escribir estos devlogs será bastante casual, no me gusta usar muchos términos técnicos o cosas por el estilo.
Tech Stack
Supongo que la mayoría estará más interesada en el tech stack detrás de la aplicación. Lore Designer es una aplicación de escritorio multiplataforma (de momento, es probada en Windows y Linux, existen binarios disponibles para macOS, pero como no tengo una para probarlo, no estoy seguro si funcionan).
- Usa Tauri v2, por lo tanto, Rust como backend.
- Nuxt.js como frontend.
- Shadcn-Vue para los componentes
- SQLite como base de datos
- Y más cosas que verán más adelante, pero eso en esencia.
¿Qué más hay por ahí?
En el mercado existen multiples soluciones para la planificación de videojuegos, desde aplicaciones web, de escritorio, etc. Puedes encontrar varios en Itch.io. Aunque he notado que pocas de por sí se centran solo en videojuegos, de las que conozco, hay una bastante interesante llamado draft, pero es de pago. También existe el famoso Articy:draft que cuesta un ojo de la cara.
También he notado que la mayoría se centra en una u otra cosa, o se enfoca en la planificación o se centra en el diseño de los diálogos u otras áreas. Y tiene sentido, por supuesto, es muy difícil abarcarlo todo.
Por ejemplo, existe Dialogue Designer, el cual es una herramienta para crear diálogos de rama que funciona con multiples motores de juegos, también existe otro más popular llamado Twine.
En mi caso, quería crear una aplicación que intentara abarcar lo justo y necesario, desde planificación, incluyendo la creación de GDD (Game Design Document), manejo de presupuesto, tareas, progresos, fechas, hasta la narrativa, como personajes hasta historias lineales y de rama.
Metas y objetivos
Mi objetivo es crear un MVP (Producto Mínimo Viable) de esta aplicación, con las características necesarias para su despliegue inicial. Planeo obtener feedback de los usuarios y con base en eso, seguir progresando hasta tener un producto estable. No planeo ganar dinero con esta aplicación, no le encuentro mucho sentido venderla, por ende sería gratuita.
He pensado mucho en las features que podría tener el MVP, y concluí que estas serían suficientes:
- Creación y edición de personajes
- Creación y edición de proyectos (WIP)
- Internalización, de por sí la aplicación soporta inglés y español gracias a Nuxt I18n
- Personalización del tema de la aplicación (WIP, hasta ahora solo se puede cambiar de tema oscuro/brillante destroza ojos y la fuente de texto)
- Manejo de diálogos
- Exportación en formato JSON para diálogos
- Creación de GDD y exportación a PDF de este
- Tablero Kanban básico para tareas
Features disponibles
De momento, llevo el CRUD de los personajes completo, de hecho, adjuntaré un vídeo de como puedo crear un personaje en la aplicación1 y ver su información. En cierto modo, lo que más me complicó de esta área era saber con qué información bastaría. Por ejemplo, conozco una aplicación para escribir guiones o novelas llamada Story Architect, y cuando uno añade personajes, ¡tiene más de 20 campos que puedes rellenar sobre su información! O.O
Preferí mantenerlo minimalista, añadiendo una sección llamada “Notas Adicionales” en caso de que el usuario agregar algo más relacionado con el personaje.
El personaje se guarda directamente en SQLite, en una tabla llamada Characters
. Tuve que ingeniármelas con el método de guardar imágenes, dado que no puedo añadir así como así una dirección cualquiera en la DB, ¿por qué? Porque el usuario puede eliminar o renombrar esa foto, o simplemente moverla de ubicación. Por ende, me las arreglé creando un método llamado save_image
en el backend de Rust:
#[tauri::command]
pub async fn save_image(app: tauri::AppHandle) -> Result<ImageInfo, AppError> {
let file_path_result = app.dialog().file().blocking_pick_file();
let file_path = file_path_result.ok_or_else(|| AppError::FileSelectionFailed)?;
let uuid = Uuid::new_v4();
let filename = format!("{}.png", uuid);
let dest_path_result = app
.path()
.app_data_dir()
.map(|p| p.join("images").join(&filename));
match dest_path_result {
Ok(dest_path) => {
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(file_path.path, &dest_path)?;
Ok(ImageInfo {
id: uuid.to_string(),
path: dest_path.to_string_lossy().to_string(),
})
}
Err(e) => Err(AppError::from(e)),
}
}
En esencia, lo que hace este método es abrir un diálogo en la máquina del usuario, preguntándole que imagen quiere agregar, si obtenemos un resultado, se crea un nuevo UUID el cual será el nombre de la imagen de ahora en adelante en formato PNG y se guarda en la dirección app_data_dir()
, esta dirección varía dependiendo el sistema operativo del usuario:
- Linux: Resuelve a
$XDG_DATA_HOME
or$HOME/.local/share
. - macOS: Resuelve a
$HOME/Library/Application Support
. - Windows: Resuelve a
{FOLDERID_RoamingAppData}
.
Dentro de esa dirección, la cual siempre extra en una carpeta padre con el identificador de bundle de mi app, (el cual es com.lore-designer.app
) sé verífica si existe una carpeta llamada “images
”, de no existir, se crea y finalmente se guarda la imagen dentro la carpeta. De ese modo, si el usuario renombra la foto original o la cambia de ubicación, no afectará al programa, puesto que tiene su propia copia interna.
En el frontend, tengo un componente de Vue llamado ImageUploader
, el cual llama a este método
const uploadImage = async (event: Event) => {
event.preventDefault()
try {
const result = await invoke('save_image', { characterId: props.characterId }) as { id: string, path: string }
const convertedPath = convertFileSrc(result.path)
await imageStore.saveImage({id: result.id, path: convertedPath})
imagePreview.value = convertedPath
emit('update:image', {
id: result.id,
path: convertedPath
})
toast({
title: t('imageUploader.successTitle'),
description: t('imageUploader.successDescription'),
})
} catch (error) {
toast({
title: t('imageUploader.errorTitle'),
description: t('imageUploader.errorDescription'),
variant: 'destructive',
})
}
}
Notarán que al resultado final que entrega el comando de Rust, se modifica con convertFileSrc
, este es un método especial de Tauri. Según la descripción de este método:
Convierte una ruta de archivo de dispositivo en una URL que pueda ser cargada por el webview. Ten en cuenta que
asset:
yhttp://asset.localhost
deben ser añadidos atauri.security.csp
entauri.conf.json
. Ejemplo de valor CSP:"csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost"
para usar el protocoloasset
en fuentes de imágenes. Además,asset
debe ser añadido atauri.allowlist.protocol
entauri.conf.json
y su alcance de acceso debe definirse en el arrayassetScope
en el mismo objeto de protocolo.
En resumen, el WebView de Tauri solo puede cargar ciertos tipos de rutas, así que para cargar una imagen de algún directorio se debe usar este método para que funcione. Luego finalmente se puede guardar en la base de datos de manera segura.
Diálogos…
Ufff, ¿cómo empiezo con esto? Es la feature con más bugs de la aplicación, pero al mismo tiempo la más interesante para mí. Me baso mucho en el diseño de la app de Dialogue Designer que mencione antes, pero planeo modificarlo a futuro. De momento, se puede hacer lo siguiente:
- Asignar variables
- Crear diálogos y asignarles un personaje
- Moverse libremente mediante el mapa
- Conectar diálogos entre sí
Utilizo Vue Flow para crear el editor, pero nunca he hecho este tipo de editores, por ende ha sido bastante trabajo para lograr lo ya mencionado. Aún faltan muchas cosas por hacer, como la exportación de los diálogos, la importación, la creación de nodos de decisión, etc.
¿Qué sigue?
Cada cierto tiempo, estaré publicando más devlogs. No mencione todas las features que posee actualmente, dado que haría el post muy largo, pero mencione las que encuentro más importantes.
De todas formas, cualquiera puede probar la versión alpha de la aplicación en Github, donde también pueden contribuir si lo encuentran necesario.
De ahora en adelante me enfocaré principalmente en la feature de proyectos, donde uno debe ser capaz de crear GDD, asociar personajes, tareas, y un largo etc. También planeo mejorar la feature de diálogos, añadiendo más cosas como mencioné antes.
Si llegaste hasta aquí, gracias por leer. Si tienes alguna sugerencia, crítica o algo que quieras decirme, no dudes en hacerlo. ¡Nos vemos en el próximo devlog!