diff --git a/.gitignore b/.gitignore index 14ab211..ec24193 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,13 @@ save.json.tmp *.roto save.test-*.json test_snapshots/ + +# Tripo: la key vive en el entorno, los GLB y el visor en la notebook del demo. +.env +.env.* +assets/tripo/ +vendor/model-viewer.min.js + +# Capturas de documentacion: solo los PNG finales se versionan. +.playwright-cli/ +output/playwright/ diff --git a/README.md b/README.md index 643b3da..00e7415 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,166 @@ # RUNA -Un RPG ASCII para terminal donde exploras una ciudad, recorres la pradera y escribes las reglas que usa tu personaje al combatir. +**Un videojuego donde los gráficos evolucionan con vos: un regalo para quienes crecen mientras expanden su propia imaginación.** -**Versión actual: 0.2.0 — Reinos y exploración** +RUNA parte de un mundo ASCII: explorás dos reinos, conseguís equipo y escribís +las reglas que usa tu personaje al combatir. La progresión visual por nivel +es la dirección del proyecto. Hoy el sprite refleja el equipo que llevás y +los modelos generados con Tripo aparecen como animaciones ASCII en el menú, +la creación de personaje y las vistas del yelmo; todavía no hay un cambio +automático de estilo gráfico al subir de nivel. + +**Versión actual: 0.2.0 — Reinos, exploración y modelos Tripo en ASCII** El mundo corre sobre **Bare**, se dibuja con **bare-tui** y mantiene todo el arte dentro de una grilla ASCII estable. El personaje, los NPC y los monstruos se mueven sin borrar el terreno ni romper las líneas de la consola. -![Menú principal de RUNA](docs/screens/menu.png) +![Menú principal de RUNA](docs/screens/tripo-paisaje.png) + +## Actualización para el hackathon de Tripo + +[Ver la demo grabada (MP4, 68 segundos)](docs/demo/runa-tripo-demo.mp4). +Es un recorrido reproducible por el render real del juego, grabado en un +navegador a partir de entradas y cuadros de Runa. Usa una partida de muestra +en memoria, sin tocar guardados. [Cómo se grabó](docs/demo-walkthrough.md). + +RUNA incorpora modelos 3D generados con Tripo **dentro de la terminal**. +Generamos un héroe medieval, un Coloso de piedra, un yelmo de hierro y un paisaje del reino con +texto a 3D, descargamos los GLB y convertimos sus geometrías a cuadros ASCII. +El juego reproduce esos cuadros: no necesita navegador, GPU, API key ni una +conexión a Tripo para mostrarlos. Los cuadros ya están incluidos en el repo. + +### Qué cambió y dónde verlo + +| Modelo generado con Tripo | Integración en RUNA | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| Paisaje del reino | Gira en el menú. Se combina con el héroe y el Coloso ya generados para componer el enfrentamiento fuera de las murallas. | +| Coloso de piedra | Aparece en su arena, reutilizando el modelo generado y conservando la lógica de combate. | +| Héroe con espada y escudo | Gira al crear un personaje y forma parte de la escena del menú. | +| Yelmo de hierro | Gira al seleccionarlo en la armería o en la mochila, cuando hay espacio en la terminal. | + +Cada modelo tiene **24 vistas separadas por 15 grados**. Una vuelta tarda +aproximadamente tres segundos. Los cuadros del menú existen en `64×22`, `40×10` y `28×5` caracteres y se +elige el mayor que cabe sin ocultar los controles. Maximizar la terminal +permite ver más detalle. + +**El reino y el enfrentamiento en el menú.** El diorama del reino se completa +offline con las geometrías originales del héroe y el Coloso. Tripo generó el +castillo sin los combatientes del prompt; reutilizamos sus modelos para +completar la escena. Es una composición de figuras enfrentadas, sin rig. + +![Paisaje del reino con héroe y Coloso en el menú de RUNA](docs/screens/tripo-paisaje.png) + +**Coloso dentro del mapa.** Su dibujo utiliza ahora el GLB real convertido a +un lienzo `43×13`. La vida, los ataques, los avisos y las colisiones siguen +controlados por el juego. + +![Coloso de Tripo dentro de las ruinas volcánicas](docs/screens/tripo-coloso-mapa.png) + +**Creación de personaje.** El héroe generado es una vista previa; el personaje +que camina conserva el sprite compacto, su inicial y el equipo que posee. + +![Héroe de Tripo durante la creación de personaje](docs/screens/tripo-personaje.png) + +**Yelmo en la armería y en el inventario.** Es la representación del objeto +`iron_helmet` que ya existía: requiere nivel 3, cuesta 75 de oro y aporta +defensa `+2` y velocidad `-0.04`. Se compra con `Enter` en la armería; desde +la mochila (`I`), `Enter` lo equipa y `X` lo quita. La vista previa no cambia +el equilibrio del equipo. + +![Yelmo de Tripo seleccionado en la armería](docs/screens/tripo-yelmo-tienda.png) + +![Yelmo de Tripo equipado y seleccionado en la mochila](docs/screens/tripo-yelmo-inventario.png) + +**Los modelos antes de convertirlos a texto.** El visor local es una herramienta +opcional para inspeccionar los GLB originales; el juego funciona sin él. + +![GLB reales del yelmo, héroe, Coloso y paisaje generados con Tripo](docs/screens/tripo-modelos.png) + +### Cómo lo hicimos + +```text +Prompt de texto + -> Tripo: modelo P1-20260311, 3000 caras, textura y PBR + -> tarea completada y descarga del GLB + -> conversión offline: geometría, luz y 24 ángulos + -> JSON con cuadros ASCII + -> reloj del juego + bare-tui -> animación en la terminal +``` + +1. [tripo-generate.js](scripts/tripo-generate.js) crea una tarea en la API v3, + consulta su progreso y descarga el GLB cuando termina. Usamos + `P1-20260311`, `face_limit: 3000`, `texture: true` y `pbr: true`. Cada uno + de estos cuatro pedidos reales consumió 40 créditos según la respuesta de + la API. Un pedido interrumpido puede retomarse con `--task`, sin crear otro. +2. [glb-to-ascii.py](scripts/glb-to-ascii.py) lee la escena GLB y aplica las + transformaciones de los nodos. Rota la geometría, proyecta los triángulos + con un buffer de profundidad y convierte la iluminación de sus superficies + en caracteres de la escala `.:-=+*#%@`. Compensa la proporción de las + celdas de terminal, que son más altas que anchas. No muestrea las texturas: + el ASCII conserva la forma y la luz, con el color aplicado por el juego. +3. [assets/ascii](assets/ascii) contiene las 24 vistas de cada modelo en tres + tamaños. [render.js](lib/render.js) las muestra y el reloj de + [game.js](lib/game.js) avanza la animación cada 125 ms aproximadamente. + **Durante el juego solo se carga texto; el procesamiento 3D ocurre antes.** + +Los modelos Tripo se usan en el menú, la creación, las vistas del yelmo y el mapa del Coloso. +No hicimos rig ni animación esquelética: el movimiento nuevo es una rotación +de vistas precalculadas. + +### Probar la actualización + +```bash +npm install +npm start -- --solo +``` + +Esperá en el menú para ver el paisaje y elegí **Nueva partida** para ver +al héroe. Para inspeccionar el yelmo en detalle, seleccioná la pieza en la +armería o, después de comprarla, en la mochila. No hace falta generar nada +para probar las animaciones incluidas. + +Para generar un modelo propio, usá Node 18 o posterior y guardá tu key en +`~/.config/tripo/key.txt` (en Windows, `%USERPROFILE%\.config\tripo\key.txt`) +o en la variable `TRIPO_API_KEY`. La credencial permanece fuera del repo: + +```bash +npm run tripo -- --balance +npm run tripo -- --name yelmo "a low-poly iron helmet with a single rune on the brow, game prop" +python3 scripts/glb-to-ascii.py assets/tripo/yelmo.glb --output assets/ascii/yelmo.json +``` + +La conversión requiere Python 3 y `numpy`. En Ubuntu, podés preparar un entorno +fuera del repositorio: + +```bash +python3 -m venv ~/.venvs/runa-ascii +source ~/.venvs/runa-ascii/bin/activate +python3 -m pip install numpy +``` + +Activá ese entorno antes de ejecutar el conversor. Los GLB se guardan en +`assets/tripo/`, fuera de git. Los JSON se versionan para que cualquier persona +pueda jugar sin credenciales. Para ver los GLB originales: + +```bash +node scripts/tripo-viewer.js --fetch-lib +npm run tripo:viewer +``` + +Abrí `http://127.0.0.1:4173/`. El visor detecta modelos nuevos automáticamente. +En esta máquina usamos `node --use-system-ca scripts/tripo-generate.js ...` +con Node 24 para que HTTPS reconozca los certificados del sistema. + +Los prompts utilizados para los tres modelos, el flujo de regeneración y las +limitaciones están documentados en [ascii-turntable.md](docs/ascii-turntable.md) +y [tripo-integration.md](docs/tripo-integration.md). + +### Portada ilustrada + +También creamos una portada gráfica con una herramienta de generación de +imágenes independiente de Tripo. Se conserva como material visual del proyecto; +la pantalla del juego utiliza ASCII. + +![Portada ilustrada de RUNA: héroe, Coloso y torre central](assets/runa-cover.png) ## Novedades de la versión 0.2.0 @@ -223,8 +377,8 @@ npx bare test/map.smoke.js Estado revisado de esta versión: -- `140/140` pruebas correctas. -- `1309/1309` aserciones correctas. +- `142/142` pruebas correctas. +- `1337/1337` aserciones correctas. - Formato y lint limpios. - RUNA, NOX, fronteras, puertas, portón, pradera y dungeon validados por el smoke test. - Capturas inspeccionadas y recortadas al borde exacto de la terminal. @@ -239,6 +393,13 @@ npx bare scripts/readme-screens.js .readme-screens Esto evita documentar una ciudad, un héroe o un combate que ya no coincidan con el código. +Las capturas nuevas `tripo-*.png` usan estados reproducibles de `120×44` +celdas. Se captura el elemento `.terminal` de los HTML generados, con un +viewport suficientemente grande para mostrarlo entero. `tripo-modelos.png` +es una captura del visor local con los tres GLB reales cargados. Los estados +de tienda e inventario se preparan con equipo para mostrar las vistas previas; +no representan el inventario de una partida nueva. + ## Arquitectura | Archivo | Responsabilidad | diff --git a/assets/ascii/coloso-field.json b/assets/ascii/coloso-field.json new file mode 100644 index 0000000..041d240 --- /dev/null +++ b/assets/ascii/coloso-field.json @@ -0,0 +1,372 @@ +{ + "source": "coloso.glb", + "frameMs": 125, + "variants": [ + { + "width": 43, + "height": 13, + "frames": [ + [ + " ", + " %*##* ", + " %%%%*- ", + " =*%%+-=: ", + " **+==-- ", + " #%*%= ", + " -=+#-+ ", + " #***+ ", + " +#*+- ", + " %%*-+ ", + " =*=+ ", + " ##%%- ", + " " + ], + [ + " ", + " %%%+** ", + " *=##%%##+ ", + " ++#=#%##:= ", + " *==#==: ", + " %#***%-# ", + " *:%--==# ", + " #*%%#+*+ ", + " *=+#=*+= ", + " ##%#+#+ ", + " -++- ", + " ####%= ", + " " + ], + [ + " ", + " ####%#+** ", + " #%+#=%#%#**= ", + " %*=+:#*#=%##++ ", + " =***-==#- ", + " %%--##*+#*## ", + " *=--=%#=-+=# ", + " *+#*+%#*+*+- ", + " +++=:**=%#*: ", + " #++%#=* ", + " =:++#= ", + " ##:+###- ", + " " + ], + [ + " ", + " %### ##=%%+= ", + " #%%+##+%%%%#++ ", + " %#**#:-####+#*-+ ", + " =+==*=--+*** ", + " %%-+ +#*+-#%+** ", + " :+==%--+#*++-+- ", + " *=-##+-+#*+ *-- ", + " #=%++-=+*#-:=#* ", + " ##==#%*-* ", + " =-+ -*- ", + " **- **** ", + " " + ], + [ + " ", + " %*# #*- *#=- ", + " *###%+%%+%%%%**= ", + " %#*+**:-#*=%##*+ ", + " *+++-*==-:++*= ", + " %#=- *#+=: %#=+ ", + " -+- %---%#+=+::: ", + " #+-* #*=-#+#* +*: ", + " :%%+==: *++=: %# ", + " ###- *##++ ", + " +-= =*: ", + " *** *=+++ ", + " " + ], + [ + " ", + " %#** #+- *#%: ", + " +###%+###=#%###*- ", + " %#*=*#*:=*+-*+*+= ", + " *=* -=*---- +++ ", + " %%#= +##+- %%%= ", + " --=* %=--:%*- ++-- ", + " %+== ##*-*##== -++ ", + " ##- *==-**+=+- +#* ", + " #**: *%*#= ", + " +-+ ++ ", + " #*++ #-== ", + " " + ], + [ + " ", + " %#% %*= #+*# ", + " %%##*%=###=*%*#*= ", + " *##+-##=-=+=#-#*= ", + " *-= =-=*+-= *== ", + " %%*+ **#-- #%#% ", + " =-- %*=:-:+ +-= ", + " #== #=+-+*#++ # += ", + " =# =+=-+++++== *# ", + " %##+ + *%*: ", + " +- == ", + " ###= ##- ", + " " + ], + [ + " ", + " #%# ##- #### ", + " %%%**+%-***-#%+*+ ", + " *=**=+#*+-=+=*-*+ ", + " **- +=-*+-- -*= ", + " #%#+ ****= ##%= ", + " +-=- #%%=--:# :+++ ", + " #-: %%+=+*#==# -=- ", + " ** *++:++===- *=* ", + " *##=+ ##= ", + " *- ==- ", + " %%#- **# ", + " " + ], + [ + " ", + " #%%# +%+ %%%# ", + " %%-+*=%:++:=#=+= ", + " #-++*-**-=--*+== ", + " **-=+=-*== =+- ", + " #**= **++- %##+# ", + " +--=%%%+==+# +== ", + " +*- %%%-+***# -=- ", + " += ##=-= =---%++ ", + " %%**= *##- ", + " +++ =-- ", + " %%*- *** ", + " " + ], + [ + " ", + " %%#* =#- %%#% ", + " %%%-=+#-==-#== ", + " -%#-+=+-*++:+:- ", + " *+===---+==- ", + " %#** #*+== %##*# ", + " ++:*%%#:==*%-=- ", + " %#= %%*=-*+-=-- ", + " ===#%#-=+=--== ", + " +#*+-*##*- ", + " +=- ++: ", + " #=%+= +++ ", + " " + ], + [ + " ", + " %%#=%+#%#% ", + " %%%#--*=-=#- ", + " -%%%-=-=+*=--- ", + " -*===-=-== ", + " ##+=#+*-%##+ ", + " ++-%%*-+-+:+ ", + " %+#%%%*=*=+- ", + " :+=%%#:+=-*- ", + " #%#=##++ ", + " *=* =- ", + " #%%*###== ", + " " + ], + [ + " ", + " %%%%*%%# ", + " #%%%%=-+#- ", + " =%%%*-:-+- ", + " =**--=++ ", + " #*#---#= ", + " %*+-#+*+= ", + " %=%%#++- ", + " ++%%#--- ", + " *#*-*= ", + " =+:== ", + " %*###-- ", + " " + ], + [ + " ", + " %%##* ", + " %%###= ", + " ==%%#+-- ", + " **=**:= ", + " *##+- ", + " -%#--= ", + " %%*:- ", + " =%#-= ", + " =+#+: ", + " =*-+ ", + " %+#-- ", + " " + ], + [ + " ", + " %*%##* ", + " *%%%%##*- ", + " +-#+#%##-= ", + " **+-=++ ", + " ##*+%#=+ ", + " %+=%#%#: ", + " #-*%+%-# ", + " #*##++-- ", + " =-=++*= ", + " +++= ", + " %%%%=- ", + " " + ], + [ + " ", + " ###%=+%** ", + " #%=*#%%%#**- ", + " =--+%%####**:+ ", + " ***+-+=== ", + " -%#***=****= ", + " %#*#:#%#+%*: ", + " %+#+#=%#+%** ", + " -#%*#=##-+** ", + " +#+++++ ", + " ==-==- ", + " ##%+%%#+ ", + " " + ], + [ + " ", + " #*#+%+= %#*= ", + " #%+%%#*#%%%*+= ", + " +*=##=#%#-##*+++ ", + " =:*#=-===++- ", + " %=#*%#*+-+ ***+ ", + " %+++##-##*=*%++ ", + " #*+ %##-%**+#+= ", + " -###+##*##*-*#* ", + " -+#=-++== ", + " -=- +=- ", + " *#++ ##= ", + " " + ], + [ + " ", + " *+#= %%- *%= ", + " %%%*%##+=-%%%#*- ", + " %*###==#%#-**+== ", + " =-+:##*--+-=+: ", + " %=%+ ##*=- %*%= ", + " %=++#-#+###+ **+ ", + " %-* %+##*%++ +*== ", + " *# #+=## *#+=-## ", + " =+##+ *=-- ", + " =-+ +== ", + " #*#== %#+ ", + " " + ], + [ + " ", + " *%## %++ %%%- ", + " ##%##%**==%=##%#+ ", + " *%+##%%--*#*+=**- ", + " ++= -**=+-: -== ", + " %==- *##+= *+=+ ", + " %#-# %:+#=*#* +*#- ", + " %-* %%###*%== +#-- ", + " *-= ##-###+**= %-- ", + " =+-:# *+=: ", + " =- ++- ", + " %%=- *#** ", + " " + ], + [ + " ", + " *%#: +%- %*% ", + " %%##+*:##-###**#+ ", + " *+##-###-+++-+-*- ", + " ++= -*#*-+= =-= ", + " %--- *#==- *%=* ", + " %#: ##+*-+# **= ", + " #: = %%-*****+ +#+ ", + " *= *##-****+*- -= ", + " *==+ * %+:- ", + " =: =: ", + " %%+ #%#+ ", + " " + ], + [ + " ", + " ##%* *%* #*# ", + " %=*##%%##%%#-%**+ ", + " **+-=*##-:=*=+++= ", + " ==+ *=*#+=- =-: ", + " %#-- +*++- ##=+ ", + " #%*- %**++=+= -*++ ", + " *%% -%%#-++-*+ %=# ", + " -+- ##-=+++*=# %- ", + " *=- *#%=- ", + " +-+ += ", + " %%# #%%= ", + " " + ], + [ + " ", + " %#%% *** %%## ", + " %%-#**#%#+%*-%=+ ", + " %*#=*+#**----++= ", + " *== =*++*=-=-= ", + " #%*:* *#*=- *#*- ", + " ##+ %%+*==--=++% ", + " #%% +%%*===*= %=# ", + " ---#%** ==+=- %= ", + " #*-: -+%*= ", + " +-+ :+- ", + " %#* #%%+ ", + " " + ], + [ + " ", + " %%%% #*% ##%# ", + " %%%%=+#**%+-%= ", + " %**=--#+*=::--- ", + " *--==*+=---- ", + " %#%+- +#*+- **#- ", + " ###*%%%*+=--++- ", + " #%%-%%+#--= -## ", + " -:=%++---**%%* ", + " ##+:=*-#*+ ", + " *:+ :+- ", + " ##* ####= ", + " " + ], + [ + " ", + " %%#%#*#%## ", + " %%%%+=-+%-#: ", + " =%%++==*=+=--- ", + " **:==+-+-- ", + " %*%-##*=-+** ", + " :*#*%%#=-:== ", + " #%#-%=#::-*= ", + " -=:-#=#:+=%+ ", + " ##+-+*:* ", + " ** -:= ", + " #%#*=**## ", + " " + ], + [ + " ", + " =%%*=-#* ", + " %%%%#=-#** ", + " -%%+==--*- ", + " **--==-= ", + " %-#-#+-+ ", + " -**#%*--* ", + " ###*-=%* ", + " -----==* ", + " %#%==- ", + " *+--= ", + " ##%*+** ", + " " + ] + ] + } + ] +} diff --git a/assets/ascii/coloso.json b/assets/ascii/coloso.json new file mode 100644 index 0000000..c7c6725 --- /dev/null +++ b/assets/ascii/coloso.json @@ -0,0 +1,1056 @@ +{ + "source": "coloso.glb", + "frameMs": 125, + "variants": [ + { + "width": 64, + "height": 22, + "frames": [ + [ + " ", + " ##%%*+ ", + " *#%**%#*=+= ", + " #-%%%%%%--*+ ", + " ##%%%%%%%**--= ", + " ==*%%%%%**==:: ", + " ==+%%***=++-- ", + " +=##+====-=- ", + " -%%#+*#-=== ", + " ##*--%**-=- ", + " =*+=*=*--=*- ", + " --+%***** ", + " ==+%#=-+* ", + " +#%#***-+- ", + " ++=--*--- ", + " %:-==#-- ", + " #%%**%--+= ", + " ==+-*-=+ ", + " ***=++ ", + " ##+%*+ ", + " ########-= ", + " " + ], + [ + " ", + " % *#*#** ", + " ##%##*=#%-+-#*+== ", + " =%*%#%#%:%#####-- ", + " *%+##%#%%%%####++ ", + " *=*-##==#%%###**== ", + " +=-=*+++*##%++++ ", + " -=*===#%*==-- ", + " **====%%%#*+# ", + " %%#%*##%*%-#*++ ", + " +++=*#+=#===+=- ", + " -+=:%%%--*%++# ", + " +##*=--=+%*%-+ ", + " ===+#%%##**+-+ ", + " *===+++#-*:*:- ", + " ---*---==*- ", + " ##*%###+##= ", + " ==-+=+:+-- ", + " --=*#+-* ", + " ###**##+ ", + " #####***##-- ", + " " + ], + [ + " ", + " # ***+# ", + " %**%**++#%%=**%++#*=-: ", + " -#%%##%%#+%%#%-%#***-- ", + " #*%=#####*%%%%%%#****= ", + " %#*==+=:*##*+==#%%###*++ ", + " *+-+==**+++=-**###=++ ", + " =*--=+=-===#%+-+ ", + " %**--+++====-+#%%**=# ", + " %%%%*%%=###+++#%+*=-#= ", + " +++=#-+###=--*+##=-=- ", + " :---*-=-=*%%##=--=%%= ", + " ==-*###*=====#*=*%+#== ", + " *-=++==-+*###*:+**+=+ ", + " #=+++====***==%%-#-*+ ", + " * :--:=%--=--*= ", + " ###++*%%#*=#* ", + " ==:::-+==-== ", + " ==+- =*#=: ", + " ####- **## ", + " ####:--##**#*** ", + " " + ], + [ + " ", + " ** *#*=* ", + " %%++%++%==*#*%%-=+*+%===*: ", + " *=#%%%##%%%##=%%%%%*##*++= ", + " ###%%*##%#####%%%%%%###+++- ", + " %##*++*#*::-*##*#%+++##***#+ ", + " *=++*+-+**++++-*******-* ", + " *=++=-===+===--=+**#- ", + " #***-: =++==----=+#*%#*-* ", + " %%%%+#%= ###*++--=#%##+*==- ", + " *++:+#- =####*--#*=*+#*=:- ", + " :-===+*:=--=+*##**+=---=#- ", + " ==-:=#####+-*#==+*+= ==#%=- ", + " #*===%+++--:++**###+= *+=-: ", + " #-+*++++---+++**#-+-%%:#=* ", + " *+ -----+ *%-==--= ", + " ####=== ##%%*+-* ", + " ===--= =+--=- ", + " =--+: =+*-- ", + " ##**= *++# ", + " ****-= ****++#++= ", + " " + ], + [ + " ", + " # =*++: ", + " %%=%%=-%=-%##*%%*--+++=#-=# ", + " +==####*##%%%%##-%%%%%#%=*+=+ ", + " %######**#%%%%###**%%%%##%#*== ", + " +%##**++*#*::-*****=#%+#+#*++* ", + " #*++++=+-+#*==++=-***++*++ ", + " +=++++-=======-=::+*+*** ", + " %%#+*= =+++===--= **+%%%= ", + " *%###=*- =###*==:: %%%*+=+: ", + " #*==-=- #####*#:=#* =+**#-:- ", + " -:--+= #*-+----+*##+*= -:::=#- ", + " =--:=** *###*=-*##*-++= ---#-- ", + " ++++ *+++==---*++++*+*= #*==: ", + " #:+ %%+++==-:+ *+++++:- %%##++ ", + " #* --:--* **:#--=-= ", + " *####-- #####+-+ ", + " =+---=- ===-:- ", + " +--*- +++*- ", + " **** ###=+ ", + " #**** **=++=#== ", + " " + ], + [ + " ", + " = *#== ", + " %%%-%#::%: %##+#%# :::++=*:: ", + " ====###*%##%%%%###:#%%#%##**=* ", + " ########***#########+**##%%##**-- ", + " *%###**=*##*-::-**++*-*##++*++= ", + " ##*==+= ++-+***+*====#***++== ", + " *==== =-=====+-----+ *+++* ", + " %%#++++ ==++++---+ **=%#+- ", + " +#%###+ +*###+--- *%%***-= ", + " ##*==== #:####+#-+# ==+#*-= ", + " +--==* %*=+=---==***=+ -----%* ", + " =::-+= =##**-=*+###:+= ----*+ ", + " #++ * #++====**=+++=**+ *-- ", + " ##=%% =**+===+***=++=++- ###=* ", + " #* ---:-== ****-%%--++ ", + " ###***- ***%%*##== ", + " +++---+ +++=-- ", + " *---+ ++=+= ", + " #**+++ ####-* ", + " ***+++ ###=---- ", + " " + ], + [ + " ", + " = *+*- ", + " -%%%:%# % ##**=* # ::==+:+ ", + " ==+--#** %***%%%%%*** %#*##**= ", + " %%%######***#######***++****##*%= ", + " #*%####+-+###+=-::+*==+##**%=#= ", + " %##*-=- ++++-=**-=+--+ #**==+- ", + " %*-== ==-==*==----== *===+ ", + " #%%%%== ===++++::= =**--#*: ", + " -*#%***= ++####=- *%%%%+=- ", + " +###--- %-###***=*=* =====+= ", + " %=--=+ %%%+*+::::--*++= ---=-+ ", + " ==--* #=+*++-++*####-- %----- ", + " #*= *- %#+==--+++==+++=+= **#= ", + " =*%% +++=--+++++=+++===+ %##+ ", + " * #--:-=+ +++++-:%*-* ", + " %####++= ++ *%%+**: ", + " *++:::* ====-= ", + " +*--- +++-+ ", + " #++== ####- ", + " ##*##=== ###---- ", + " " + ], + [ + " ", + " % +*=- ", + " %+%#%%=# %%##*+* #*#%--==== ", + " %%+-+--*++#*++%####**+-%%+**++ ", + " *%%%%##****+++*******++==-+++**+% ", + " *=*#****=++##*=---==+-=**-+*-+ ", + " %%**+=:+%=++==+++==:-*=***-=: ", + " #*--- -++==--+++---+ --*==- ", + " #%%#%-- ========-- ==**:%* ", + " -*=#%+++ *+*#**-= %##%%#-- ", + " ++**:-- #-=*##*++++* +======+ ", + " %%+--+ #%%%+==----:=+= :---+= ", + " %%==== #*+++==++*####+ %#=--= ", + " #%* +* %%*+=::*++=====-= + #-+= ", + " *** ++++=::+++=====--- %%** ", + " * =-=--==++++ -=-%%-= ", + " %%###===++ +####++ ", + " *+++--* =+===:+ ", + " ***== ++=+: ", + " ##=-- #####- ", + " ##*###--- ****== ", + " " + ], + [ + " ", + " % ++*= ", + " %%+=%#### #%**+=+#%*%#==*-- ", + " %%%%+-+--==%*==****++-#%%=+++ ", + " #%%%%#*++***+++**+++==--:===+=# ", + " ###-++++*--+**+-===-=-**+-=+: ", + " #%%+**+%%*===--==-=-=++++-: ", + " ##*---=++++--*++=== ==+--- ", + " %#%%##-- -====-----= =+++%* ", + " %-#*###= =**++#++- %#####*: ", + " *+=**=== #===+##+=-* ++----- ", + " *%*+--* %%%%#=-====-*=- %--=-* ", + " %#%--+ %%====-=******=#-#-:--+ ", + " #%%-+ *%%*=---=+==----: *--+* ", + " ++=+ #=+===--== ===----*%%++ ", + " ++ #:==--+== -==-%%+ ", + " %#%#***- *#####= ", + " *++==== *=+---= ", + " ++++ +===- ", + " =##:- ***** ", + " #####*#-- +***= ", + " " + ], + [ + " ", + " % %++: ", + " %#%%-=%#**+%#*-=%%+%#+++: ", + " %%%%%+=+==-#*--+++==#%#-=- ", + " *##%%%%%++=++--=====---:--==* ", + " =#%##--++=+:-**=:++:-+==:-#- ", + " #%%%=++=-*---:----=+===- ", + " *%++======+--**++===-- ", + " %#%%*#= -++==----+ -===%% ", + " %#-#****-+##*+=#== *%##***- ", + " #++=++===%==-+**--+ #*+::-- ", + " #%%+--:#%%%%##:-====- %--=== ", + " *%%=-- %%%===--***+++-#--:: ", + " %%%%= %+%%*+=-=+==------+== ", + " -==== #%=+---==+==---=%%== ", + " == +++-===* -=-=%% ", + " %#%**++- #*##*** ", + " **++=-+* #=++::+ ", + " **==* ++-= ", + " =*** *+++* ", + " #####*##= ***++++ ", + " " + ], + [ + " ", + " %%%+=+- ", + " =%+%%-=#%++%#:%%=%*++- ", + " %%%%%%+++++**-==%:##:- ", + " ####%%###=---:-----#-:-+ ", + " =%%%%%*--=---++-*-=-=-:* ", + " %%%#-+===-::*--*--- ", + " =%%+=+======**==-= ", + " %%#%#++++----==%-+=-% ", + " #%#-#+++##+=---*+%##+++ ", + " #*+*-===%=---+:+%-=-+-+ ", + " ###+---%%%%*--++----++ ", + " *##%=--%%%=---**==-==- ", + " %%%%#%#*%%*%-+==--=+- ", + " +-+-+%%+=+:-++==-=*=- ", + " -=**+===+#-=-+## ", + " ##%#*==##*#+++ ", + " +*+=-:+===--* ", + " =*+== =++-- ", + " #+** *#===+ ", + " #*****+**#**#==== ", + " " + ], + [ + " ", + " %#%=== ", + " %%+%%%--%*=%%**== ", + " %%%%%%%===*+=%#* ", + " #####%%***-:::-#-- ", + " ==%%%%%%*---::-*=--* ", + " -%%###=:==-=-: ", + " *=*%=-*--++#- ", + " #%%**#++-:=+--- ", + " #%*-*=+=-+-#**== ", + " #*=*-=-:::-+==== ", + " ##%==-=#++--=+* ", + " *%#%=====::--== ", + " +%%##*%##----= ", + " +-- -%=+-=-+-=- ", + " ==-++++=:*## ", + " *+###+--**== ", + " *+*+-:-=== ", + " -+=:+=== ", + " %+++##--- ", + " #*+++++=+##--- ", + " " + ], + [ + " ", + " %%%%#- ", + " +%%%#%%--#+ ", + " #%%%%####==- ", + " #%###***#*+*=- ", + " *==%%%%##+=--- ", + " %*+=*%%**=+-= ", + " *+**=+%%-::= ", + " -++#%%#+*-+ ", + " **#%%--+--- ", + " +*+=%#==+--= ", + " #*#*#*==+ ", + " #*%*#+=+- ", + " %+%%%***-= ", + " -#+%=+:-= ", + " +=+=+=+* ", + " -=*+*##+:: ", + " =*++=--+ ", + " --*+-= ", + " %%-*=- ", + " ###====-=- ", + " " + ], + [ + " ", + " %%%%-- # ", + " ####*++#%-%##--#= ", + " %%=#%%%%%%###**-+ ", + " :*#%%#%###+++#=== ", + " +--##=+=#%%####-== ", + " *%##*+=*#%%++++= ", + " *#*-+--=++##- ", + " *-+++--%%%##+ ", + " *%****+=##%%:=- ", + " **+#==-*###-+=* ", + " :#*+-%%*%%*%*+ ", + " :%=*+%%%++%*#= ", + " %-+**+%+**##++ ", + " #-***+#*#+#=:- ", + " #*#****==+* ", + " *-%=++++*== ", + " --*+=++==+ ", + " =+=--+== ", + " %=*%%-*- ", + " ###=##=#---- ", + " " + ], + [ + " ", + " #%%#+ % ", + " =%=****#%*+***%%#%*=%* ", + " ##%+=#*+**%%%%%##***+: ", + " #*-#%%%%%%%%%%#***==*- ", + " %*--#+-%%%#+#++###***:++ ", + " --+**####+##=***#==++ ", + " -+****-+---+==## ", + " %%%%+*-**=::--+%%%**= ", + " %%#********==+#%+%%--- ", + " #+**#*=-*--##-##**-=+ ", + " -**#+##=-#%%###:%+*+* ", + " %=%-=+%##=%%#*#%*=%=*+ ", + " %==*-#*##=%%%#+#+#**= ", + " -%*%=*###=+###-+*+*:= ", + " +####=-****-= + ", + " ++#=++=+++=+- ", + " -=-*--+===-- ", + " ===-- --=- ", + " %-** %%==- ", + " ***-***#####:-- ", + " " + ], + [ + " ", + " %*%#= %# ", + " +=#-+++++=+%+=****+###%+#- ", + " ##%%=+%++#+*+%%%%%%%#**+=- ", + " ###*==%%%%%%%#%*%%%%%++*--* ", + " %#*=##%#==###%#=-##*#**+++*+ ", + " *=***=###-###=**#***+#-+ ", + " ::-*##***--====+++-** ", + " %%*%%==++#-**+---- %%##+- ", + " ##%+#++--*##**++-- +%%#=##= ", + " ##=++*+###*:#::##= *##++=- ", + " :-***+#=###*-%%##**=#%:#=* ", + " %:#%+= +%###*-%%*+***%*-%=+ ", + " *%+-: #%+###*-%%%#*+-#==++- ", + " -###+*#++###-*+##**+-*#=#- ", + " +##+###- **++:: =* ", + " ++=%#=-- +=+===: ", + " ---+== :==--- ", + " ====- +---- ", + " #=++ +%%%- ", + " *#**+++=++ ####*= ", + " " + ], + [ + " ", + " %*%#+ % ", + " +=#-%======%%#-*=#++**+###+ ", + " *%%%%%**%=##+**-#%%%%%%#%**=- ", + " #%%%*=#%#%%%%%%##%#*%%%#%+=+:* ", + " %%+=#-#%##===##%##--###*#++==* ", + " +##=##-=##+=***--+*#-**=*- ", + " =--:-**##+-**--=++-==-++ ", + " %%%%%-- +:#-+++-== %##**= ", + " ##%%+=== *###*+==- ++%%%-*+ ", + " ###--++* -###+-#+-+# =*#*=+: ", + " ----+** %-%####-+%###+- %%-##- ", + " #*-*#+ +%+####-*%%+*+* *+%+##+ ", + " ##==- %%++####-+%%%*+:*= *-*= ", + " #=*#+* #+==#### +*###+=:%+ ##= ", + " *+*==#### *+*+=- +* ", + " =++-#=## **==--- ", + " ===:++ -++==:= ", + " -==-+ =+==- ", + " %#=== -%%% ", + " ##+++==== #***+ ", + " " + ], + [ + " ", + " %%=# % ", + " ++=%-%--: -#%##-# #*+++**=*# ", + " *%#%%###*%#*#=+=+:##=%##%%#*+= ", + " #%%%%%++%%%%%%%%##%*#+#*##%%%=--- ", + " *%%++#::#%%%---***#**++#**#*=-- ", + " #**#:-+##--***+=***=+ +#-++-+ ", + " ++==- --*****=++-=:* -=-== ", + " #%%%#%: +*-*-+==-+ =%%%**+ ", + " ##%%++-+ **###++-- *+%##*+ ", + " %###:== *-####=*#=* #+*#++- ", + " ----==* %#+#####++%##*= *#%-*- ", + " *%+-** #%%=####+*%%+-= %+%%=* ", + " +%* =%%==####+**%##+- = +-+ ", + " *-+++ ##+--####+++****=- %-#-* ", + " **++--##### -#+++-+ ++ ", + " ==++-*:##* *++--:: ", + " ===--= --++-=+ ", + " ==--- +++== ", + " %%*=-- *+=##- ", + " ###----- ***+++ ", + " " + ], + [ + " ", + " %%%* % ", + " *+=*%-: % ##%**# # *==#+++ ", + " -=%##### +#-**=+=%%*# %%**##*+ ", + " %%%####+*%%#%%%###*#+##*-**##*%-: ", + " ++##=*--*#%##=--+++#++-+*-***-- ", + " %#*##-- =###:%**-=-+++ ***==== ", + " +-+== +--**#**-++:=- =--=- ", + " #-%#%%-- +**-**==== +%%%++- ", + " %#*##==- **#**==- **==##+= ", + " #%#**=- %*=#****-*=* *+***=: ", + " -----: %%###-****+###+- %##-++ ", + " #%==** #%%%--****###*-- %+##+ ", + " *=%= *-%%%-#*********+= #- =-: ", + " *--+ =####--#*******+**: %--= ", + " **=+::#**** -*%+==- * ", + " *=====: ** ***+-::- ", + " ==-+=- *--=+:: ", + " ===:= +=+++ ", + " %%%=- *+=## ", + " %%-#::- *###*#== ", + " " + ], + [ + " ", + " %%#: % ", + " ###--%-##% ##%*+*% ##--#+=- ", + " %=+#*##**#%*%#**#%%%*--%%***+= ", + " #%%######=%########+*=*+#:+***+%- ", + " ===+##-==*#####:::==*=++++-++= ", + " %#++**==#****--+++--===**+*-- ", + " *===++ -*-=***+++==-+ -=--: ", + " *%%##*= -++++-++-- +#%%##= ", + " -%****=- #***+++- %##:-#*= ", + " *#%#*++- %%=##*+++**- -*++++= ", + " #--::- %%*=*+#+++-*#*= %#*#+= ", + " #*%:-* =#:##-#+++-#*+= =%=*#+ ", + " =+#- - **###--#++++**+++ #: -%= ", + " ++:: +####-==+++++**+=+ %%: ", + " +*++--= +++++-*%=-- * ", + " #*==--- -**#**==-- ", + " =+==++- **--==- ", + " +---+ =+==* ", + " -%%%== ##=%* ", + " ####*- ######+-- ", + " " + ], + [ + " ", + " %%%# * ", + " #%###::%=%#%##%+% %%%%::*== ", + " %%%==#+**+%**#+=*#%%+--%%==+= ", + " #%%%%##****-***##***=++##:==++# ", + " %%===*+*:**#*****--=-+-+==-== ", + " %%%+=*+#%%++++=%==----++=-- ", + " %*==== ===*=+++*==::*=---= ", + " %+%#** +=++++-=*-- -+##%*#- ", + " -%%**++- *#***===- ####:#++ ", + " ####+== %%++**+==+= ==#++==- ", + " #:::-= #%%%+++*====*++ -%%**=- ", + " %*%+*+++#%#%#*=*====#+= +#%-+- ", + " #=+## #+*%##*=*====**== #==-% ", + " =----+#%***- *====+*=-- %%%- ", + " ++**++- ====**%-- =# ", + " #*=--:= ++***--= ", + " =+*--*= **::--= ", + " +*--= ++-- ", + " -%%%+ ##*+* ", + " **##+ #######-- ", + " " + ], + [ + " ", + " %%%* % ", + " #+%#-##-%+#=#%#=%%%%#=*:: ", + " -%%%%+-*=++#+=#=*%%---%%-= ", + " %%%%%%%**+++-++***++==*-:--=* ", + " =%%%+++=+-+-#+++**==:=+++-:- ", + " #%%%=-+=%%%+===#-----+== ", + " %*-=--===+*+==*---*--+ ", + " #%%%*+ ++=====--= ==+*%%* ", + " --%%#+=- +##*++--=#%+***-=- ", + " *+*#*-- =%%+++**=-==+%+++-- ", + " ##---= %%%%%==#*---==--%**+- ", + " %+*%%*-=%#%++**---+- +*#%:= ", + " *##+**%=*%**+**---+=- **-#= ", + " -=+::=%%%++=-#---+*: %#%#= ", + " ++%*== #---*%:= ++ ", + " ###+=:= *#--+**= ", + " =*+*::+ -**+-+-+ ", + " +*:: :++-# ", + " *=### **## ", + " **#***= ###**#**: ", + " " + ], + [ + " ", + " *#%#%+% ", + " #*+%%#-#*=%###-%%%#++- ", + " -%%%%%=:+-=*-##%:--##- ", + " %%%%%%%%%+-===+++=-%--:- ", + " =#%%%%++==-=*==+++==-=-: ", + " =%###--+%%*=--*--== ", + " %%*:::===++--+---* ", + " #%##%==++===----=*+#+ ", + " %-#%#-:=+##***=--+**++- ", + " =#+*#*+-=%%%==*#-:-==-- ", + " :%#-==*%%##-*#::==%+-* ", + " =%++##+-*%=*#::-+###-= ", + " *##=*++=++=#::+:==+-# ", + " =:=+:%+#===#::++%#%## ", + " #++%++--#:::#-:* ", + " ###%+--+**-:*- ", + " -=*=*---**=== ", + " *=+-- :+==- ", + " #==##* ***% ", + " #*##*#+++******** ", + " " + ], + [ + " ", + " *#%##= ", + " *#+*-%****-**##*= ", + " -%%%%%====-+#-** ", + " %%%%%%%##==--==:-- ", + " --%%%%%+++===---*:-- ", + " %%###:-+%*=::= ", + " %%*---===*-== ", + " %####*+---:--== ", + " *%-%#+#=##++-=+= ", + " =**+*+#=%%==:#:= ", + " --%##++%**##=%= ", + " -=%%=#*=#-#=#%+ ", + " ###=====#==#=* ", + " ==-==---#== #** ", + " %*-++%=-:=== ", + " #%%###=-=--- ", + " ==*=+==+=+ ", + " **+=-+-- ", + " ##=%**++* ", + " #*###+#==+#+++ ", + " " + ] + ] + }, + { + "width": 40, + "height": 10, + "frames": [ + [ + " ", + " #*#*# ", + " ##%=: ", + " *+=- ", + " *%*- ", + " =#% ", + " +*+- ", + " +-- ", + " #%* ", + " " + ], + [ + " ", + " %+##*= ", + " %*#*##- ", + " -=##= ", + " =*%:#+ ", + " #=*+*= ", + " *=*=-= ", + " =*+:* ", + " #*# ", + " " + ], + [ + " ", + " %**%#+*- ", + " #=##%%*** ", + " =-*-+#- ", + " +%=###%-= ", + " ###=*+#+= ", + " *+==*=#* ", + " =++== ", + " ## ## ", + " " + ], + [ + " ", + " %%+=*%%*=: ", + " ##-###%%+*+ ", + " ++-*=-+*- ", + " +%% #*#*=#* ", + " #**#=#+=*=# ", + " #**+- *+-=* ", + " +#= +- ", + " #* *% ", + " " + ], + [ + " ", + " #%- #-%#=- ", + " ##:#%#*#%%== ", + " ++-#=-:**= ", + " =-- =#=+ %*+ ", + " ##**#-#*- #-* ", + " #+%+==*+*:++ ", + " +#+ =-- ", + " ** #=+ ", + " " + ], + [ + " ", + " ### #+ %#- ", + " %##*####**%%-- ", + " *=*+=*-=**+ ", + " ##= +#=+ %*= ", + " +*+ **-+#- #- ", + " %# *= *++- +* ", + " ++* +=- ", + " ++ =#= ", + " " + ], + [ + " ", + " %%% #= =*= ", + " *###*###*+*#%- ", + " #- =-**-=*= ", + " +-* +##- %%+ ", + " #+ **==#+ #- ", + " =% #+-+++=+ + ", + " *+: === ", + " #=- -#- ", + " " + ], + [ + " ", + " %%* #+ ##- ", + " %#**+**+--+*= ", + " +--=-+---*- ", + " %#+ ***= %%- ", + " *- *+==#+ ##= ", + " *# #+:+ =-=%* ", + " *+- +=+ ", + " %- -# ", + " " + ], + [ + " ", + " %%# #= %%%# ", + " #%*+***+===+ ", + " #+-++-===+ ", + " -#= %+++ %%: ", + " %=- *=-=*=%#* ", + " ++ #+-==--%- ", + " *+= +- ", + " %# ** ", + " " + ], + [ + " ", + " #%###*%%%% ", + " -##%=+*=---= ", + " %===+-+=- ", + " ##*=%+## %*# ", + " %--**-=+-=+ ", + " +=##- +--- ", + " *+--*+: ", + " %*= +# ", + " " + ], + [ + " ", + " #%%#*%%%- ", + " *=##=----: ", + " *====+- ", + " #-+%--=%* ", + " #+=*+-=== ", + " -=%#-=-- ", + " *=:*- ", + " %* *= ", + " " + ], + [ + " ", + " #=%%-%# ", + " =###:-- ", + " *=-=- ", + " #*=--#+ ", + " %#++=: ", + " =---- ", + " -+-== ", + " %#+= ", + " " + ], + [ + " ", + " +-##+ ", + " ==*=: ", + " *=#= ", + " **-- ", + " %-- ", + " ==+= ", + " *=- ", + " %*= ", + " " + ], + [ + " ", + " %#=#=* ", + " :%%=+++ ", + " *+++- ", + " **#**- ", + " **#%%= ", + " -*##*: ", + " -*+=+ ", + " %%- ", + " " + ], + [ + " ", + " %%**=#+* ", + " *#%%%#++= ", + " :*:++** ", + " %****%#*= ", + " *=###=%=- ", + " =%#=%--: ", + " -*-=- ", + " %= %* ", + " " + ], + [ + " ", + " %%++-+=+++ ", + " #=%%%%%%++- ", + " =:#*==+++ ", + " #:##*#= #+= ", + " #-=%#-+=%%= ", + " -##+# %*:#= ", + " -= --- ", + " %+ %= ", + " " + ], + [ + " ", + " #%=-*- *** ", + " #%=%%%#%%%** ", + " =-:#+--+=+ ", + " #=- *#== :#- ", + " ##+ %#*%=:%%- ", + " #-#=##+%+-#+ ", + " =:- *=: ", + " %#= %% ", + " " + ], + [ + " ", + " %%- -# +=- ", + " *%%+%%%%#-#%+* ", + " +=-**+-*-=: ", + " *#- *#+= %#- ", + " %# ###*%+ +%+ ", + " *= #+## *% %# ", + " === =-+ ", + " %*- *# ", + " " + ], + [ + " ", + " **# *- -+# ", + " *%#+%###+#-*%# ", + " +=+-#*:= -+ ", + " **= ***- :#+ ", + " *+ #%**#- ## ", + " - ##-****: -= ", + " --- #-= ", + " %+- *%# ", + " " + ], + [ + " ", + " ### #* #:# ", + " #**#####**%+* ", + " #=+==*+=--- ", + " #** %*+- ##- ", + " =** **-+#* %# ", + " =:##- +-+# %* ", + " ==+ #-- ", + " %= =* ", + " " + ], + [ + " ", + " *##% ** %-* ", + " ##+*#*#*+%=+ ", + " ===*+*=--= ", + " +#+ %*=- #-+ ", + " #++#+*#=* =%* ", + " --#*+==** -= ", + " -- *+= ", + " %- *% ", + " " + ], + [ + " ", + " %#**+-%%=: ", + " %##*++*+::-= ", + " %--=*=-=- ", + " +*- %%+-+**- ", + " %#:#+#-+##= ", + " -:%+ -**%* ", + " =-=#*++ ", + " =# *%+ ", + " " + ], + [ + " ", + " #%#*#=%%- ", + " %%*+==+:-* ", + " %=:=+++ ", + " +*-%%*-** ", + " -%-#-+=+# ", + " =:+=#:=# ", + " =:+*= ", + " %# *# ", + " " + ], + [ + " ", + " #*%**#= ", + " #%%+--- ", + " %=-=- ", + " #%+-%-+ ", + " =*=+=* ", + " -#-=# ", + " ==:+* ", + " %#+* ", + " " + ] + ] + }, + { + "width": 28, + "height": 5, + "frames": [ + [ + " ", + " %* ", + " -- ", + " = ", + " " + ], + [ + " ", + " ## ", + " :* ", + " =+ ", + " " + ], + [ + " ", + " %##= ", + " :-#+ ", + " == ", + " " + ], + [ + " ", + " %#%+ ", + " +-*- ", + " -+ ", + " " + ], + [ + " ", + " #%%* ", + " +-#+ ", + " -= ", + " " + ], + [ + " ", + " ##+# ", + " = +% - ", + " += ", + " " + ], + [ + " ", + " %##** ", + " - =: - ", + " -- ", + " " + ], + [ + " ", + " *+*+ ", + " + %= = ", + " == ", + " " + ], + [ + " ", + " %=+= ", + " +%=: ", + " =+ ", + " " + ], + [ + " ", + " %==# ", + " -%=* ", + " -+ ", + " " + ], + [ + " ", + " ##-# ", + " #%+- ", + " -= ", + " " + ], + [ + " ", + " #- ", + " +* ", + " *= ", + " " + ], + [ + " ", + " ## ", + " %+ ", + " * ", + " " + ], + [ + " ", + " %# ", + " ## ", + " -+ ", + " " + ], + [ + " ", + " #%%* ", + " %:+* ", + " -- ", + " " + ], + [ + " ", + " #%%+ ", + " ++*# ", + " == ", + " " + ], + [ + " ", + " %%%# ", + " *##* ", + " ++ ", + " " + ], + [ + " ", + " %%%% ", + " % +# - ", + " *- ", + " " + ], + [ + " ", + " #%#*- ", + " % #+ + ", + " =- ", + " " + ], + [ + " ", + " ##++ ", + " # *= = ", + " +- ", + " " + ], + [ + " ", + " #%#= ", + " *%=- ", + " +- ", + " " + ], + [ + " ", + " %+*- ", + " #%=+ ", + " :- ", + " " + ], + [ + " ", + " %++- ", + " *%+- ", + " *- ", + " " + ], + [ + " ", + " %= ", + " *- ", + " =: ", + " " + ] + ] + } + ] +} diff --git a/assets/ascii/heroe.json b/assets/ascii/heroe.json new file mode 100644 index 0000000..263a401 --- /dev/null +++ b/assets/ascii/heroe.json @@ -0,0 +1,1056 @@ +{ + "source": "heroe.glb", + "frameMs": 125, + "variants": [ + { + "width": 64, + "height": 22, + "frames": [ + [ + " ", + " %########* ", + " #********=+ ", + " #******= ", + " #******= ", + " #******= ", + " %#%%%%%%%% ", + " #*+++++++-- **%%% ", + " -=+*+*###+-- -++%%% ", + " ++*###+- -++*### ", + " +*####+##=+%+### ", + " -*-##--=*-+%+### ", + " +* =%--%#==+*=*=== ", + " * %=%*## ****=== ", + " : #%%%%* %**--- ", + " ####+ *%+--- ", + " #*-#+ ", + " ##=# ", + " #### ", + " *%%%%%+ ", + " ******= ", + " " + ], + [ + " ", + " ##%########- ", + " ****++++++++- ", + " **#++++++- ", + " **#++++++- ", + " **#++++++- ", + " %#****%####### ", + " #%%+%#========-*### ", + " #--+++:++*####--**### ", + " +++++**###******** ", + " ++++-**####*%***** ", + " =--==*=#%*:******* ", + " *+ +-**###+%-*-***-- ", + " %%%-###=*%#****--- ", + " ##-****##*##****=== ", + " *=******#%***=== ", + " *=*******% ", + " *=*******# ", + " **=****** # ", + " %%#++##### ", + " *#++++**** ", + " " + ], + [ + " ", + " %%%%******** ", + " ****#*=======: ", + " *****======- ", + " *****======- ", + " *****======- ", + " %%+*****%%%###### ", + " ##%#%+***%#==-----=* ", + " #-:++++**=+=##***:=*** ", + " -++++**+=###*****+++ ", + " +++++*=::###****#++ ", + " =========#=###*+++ ", + " *+ **=#+#****#+%=#-- ", + " %%%%%%%#**#####*--- ", + " %%%##-**++***###%++ ", + " **--**++++*-+##% ", + " **--**++++ *#%% ", + " ###--**++++ ### ", + " ##---***+++ =# ", + " *%%##*%%+***## ", + " ***===**++++++ ", + " " + ], + [ + " ", + " %%%%%%++++++- ", + " #######+------ ", + " ######+----- ", + " ######+----- ", + " ######+----- ", + " #%%%+++####%%%%***** ", + " +%#%%=++***%%%*------+ ", + " %%-+++******==%##++-+ ", + " ==++*****+=-####++-- ", + " ==+******+---####++- ", + " =+====+=::::##=####+ ", + " *-#####=#****##-%#-+ ", + " %%%%%%%%%*+++##+#%%+ ", + " %%%%%###*#==++%#####%+ ", + " ####::###====#* ###%% ", + " ###:::####=== +##%% ", + " ###:::####=== +### ", + " ####::: ###=== +# ", + " #%%%**+- %%*=++# ", + " ####---- ##*===== ", + " " + ], + [ + " ", + " %%%%%%%+==== ", + " ########=----- ", + " #######=---- ", + " #######=---- ", + " #######=---- ", + " %%%#== ####--%%%%*** ", + " %%%%%+%++***%%%%%+:=== ", + " %%=++++******==%###=* ", + " =#+++*******=:#####= ", + " +++++********:===#####= ", + " -++=+====++=---####-##*##- ", + " *+-#######-+++##*+%%#=# ", + " *%%%%%%%%%%=*==####+#%## ", + " %%%%%%%###-#-===* ##### ", + " #####--####--- ##### ", + " ####-- ####-- *#### ", + " ####-- ####-- *###* ", + " ####--- ####--- +* ", + " ##%%%+=- %%#*==* ", + " #####--- ####*:-- ", + " " + ], + [ + " ", + " %%%%%%%%--- ", + " #########-== ", + " ########-== ", + " ########-== ", + " ########-== ", + " *%%*- #####= %%%%%+ ", + " -%##%%%%++****%%-%%+-++ ", + " *%=++++*******::+##= ", + " ++*++++*******=-*##### ", + " ++++*+*********=++* #### ", + " ::+*++===========** ##:*+ ## ", + " ***++**#######**=#* = ##:* ", + " ****%%%%%%%%%%%+- %######* ", + " **%%%%%%%%###---- ##### ", + " *####+**#####- *##### ", + " #####+ ####-- *#### ", + " ####+ #####- **##* ", + " #####+ #####- ++ ", + " *%%%#= %%%#-- ", + " *****= ####--- ", + " " + ], + [ + " ", + " %%%%%%%%% ", + " *********= ", + " *********: ", + " *********: ", + " *********: ", + " %#### ***** %%%## ", + " =*######%==++++%%%%%== ", + " %++-*#-===+++++++-=-** ", + " *++==*====++++++++::***** ", + " *+++++++=+++++++++* :***** ", + " *----+++#========--+ -*-+* *+ ", + " =+++=++:+=-******** # %*: ", + " =++++++%%%%%%%%%%%% %****#*+ ", + " -++*++%%%%%%%%###= ***** ", + " ++*****+++***** :***** ", + " ***** ***** ****** ", + " **** ***** ****+ ", + " ***** **** ==+ ", + " #*%%%% %%%%* ", + " +***** *****= ", + " " + ], + [ + " ", + " ##%######## ", + " ***+++++++++ ", + " **#++++++++ ", + " **#++++++++ ", + " **#++++++++ ", + " %%%*##**+++++ ##%##** ", + " ===****#*#======%%%##- ", + " %==-%==**==========-%-+ ", + " ##==%==+==+==========-+++++ ", + " #=%#=====++=========- -:++++ ", + " ##----==*++--------- --+== ** ", + " +====-==+%**=+++++*=+ +*=** ", + " ++======#%###%%%##### %*****+ ", + " =-====###########**** :***+= ", + " =====#+++++++++++++ +***+ ", + " #++++ ++++++ -+***+ ", + " **++++ ++++* ++**= ", + " *+++++ ++**** == ", + " #*+### +#%%%*- ", + " **++++- +#****- ", + " " + ], + [ + " ", + " %%%%%******* ", + " ******======== ", + " ****#======= ", + " ****#======= ", + " ****#======= ", + " %%%%%**+====###%##++ ", + " %%++++=+++*+------%##** ", + " %%-*-=%---++*--------#=- ", + " %%--%%%--=++----------==== ", + " %%%%%%-----++------------==== ", + " %%=%-----=+++:::::::- --=+-%+ ", + " ++--==--*-%%#+*=====++= - %++ ", + " ++-----=%%%*######**** %++++= ", + " ===----%%=#=*******++++ =+++= ", + " ==----###====+++==== -+++= ", + " ##==== ++==== =++== ", + " ##==== ++===+ =+=: ", + " ###==== ++=+++ -- ", + " %%%#=*** *%%####+ ", + " ####==== ***+++++ ", + " " + ], + [ + " ", + " %%%%%%%++++++ ", + " *#######------- ", + " *####**----- ", + " *####**----- ", + " *####**----- ", + " %%%%%%%++---%%%**= ", + " %%::+++++=====-:::%**=+ ", + " %%%:-=-%%-**-==-:::::--: ", + " %%%-:%%%%:::*=--::::::--- ", + " %%%%%%%:----*+-::::::----- ", + " %%%%%::::-****------ -----#- ", + " +++-===+--%%+#=*---- #-==-- ", + " +++:-:::-%%%%*********%#-==- ", + " ===:::-%%%##-=++++++== ---- ", + " ===::-#####---**---- ---- ", + " ###---***---- --- ", + " ###--- ***--- --- ", + " ###--- ***---- -- ", + " %%%%#-**#%%%#### ", + " #####---####*=== ", + " " + ], + [ + " ", + " %%%%%%%%==== ", + " *#######*----- ", + " *#####**---- ", + " *#####**---- ", + " *#####**---- ", + " %%%%%%%=-%%%+# ", + " %%%==+++++++--=---%+= ", + " %%%==+=%%=+**------ - ", + " ###==%%%%%--*+-=----- ", + " ###%%%%%----*+=------- ", + " ###%%-----***-====---%: ", + " +++====#*%%%-#-**: *#: ", + " +++=---*%%%%%+++++++::- ", + " ===---=%%%%%--=====::: ", + " ====+=#-%%%%--**:: :: ", + " %%%%%--**:: :: ", + " ####:::**::: :: ", + " ####:::**::: = ", + " %%%%##++%%%**- ", + " ######--###--- ", + " " + ], + [ + " ", + " %%%%%%%%#-- ", + " #########+-== ", + " ########+== ", + " ########+== ", + " ########+== ", + " %%%%%%%%%#* ", + " %%% *+++++==-:+#= ", + " %%% *-#%%+++-:-: - ", + " ### %%%%%%==-== ", + " %###%%%%%%==+=-== ", + " #####%%=====++-++: ", + " -=+++=++=-%%%+*=**## ", + " +++ #%%%%%##=== ", + " --- *%%%%#=*+--- ", + " === -=####=*=-- ", + " #####=*= -- ", + " #####=+---- ", + " ####=**- - ", + " %%%%%**%%+- ", + " #####*=##-- ", + " " + ], + [ + " ", + " %########* ", + " ##********= ", + " #******= ", + " #******= ", + " #******= ", + " %#%%%%%%%% ", + " **%%% %#*=======- ", + " -**%%% -=-%%++++=-- ", + " *++**** %%%%%%-- ", + " *%%*****#%%%%++= ", + " %%#++***##+++++= ", + " #===+===++%%%%# ++ ", + " **=%=== +####% * ", + " ***--- +%%%%# * ", + " ***=== +#### ", + " +#### ", + " #### ", + " #### ", + " %%%###* ", + " +****** ", + " " + ], + [ + " ", + " ###########+ ", + " =**+++++++++- ", + " **#+++++++ ", + " **#+++++++ ", + " **#+++++++ ", + " #%#**%%####### ", + " ****####%=%*=------ ", + " *******###*--##===+== ", + " ********+++######- ", + " -**%%%**+++#####*- ", + " *-*%%*****++###++= ", + " **===-***--+*##-= ++ ", + " ***-****---=####-* ", + " *#******--**####*-- ", + " **#***+++******* ", + " +=*=****** ", + " +=+==+**** ", + " - +==+***** ", + " #**%%####+ ", + " #***++++++ ", + " " + ], + [ + " ", + " %%%%#******* ", + " **=***======== ", + " ******=====- ", + " ******=====- ", + " ******=====- ", + " %%%*****=%%%##### ", + " ########%%=%%*:::::: ", + " ##########***--**--+:+ ", + " #:####*#####==-***** ", + " #####%%#####===**** ", + " :###%%%*+**##==*** ", + " ####++==:####::** -= ", + " #+##*#####*#::***-* ", + " ###########==****-= ", + " ########++**++++ ", + " **+= **--***+++ ", + " **= **--***++++ ", + " -- **---**++++ ", + " #*+***%%%#***= ", + " **++++#**===== ", + " " + ], + [ + " ", + " %%%%%%*++++++ ", + " ##++##+------- ", + " ######+----- ", + " ######+----- ", + " ######+----- ", + " %%%%++####--%%%***** ", + " %%########*%%%%%+---=* ", + " ############*++-+:*** ", + " #-#############-++++ ", + " ######%%#######--+++ ", + " %#####**%%#+##+##--+ ", + " *#####*+---#####==-: ", + " **#####*###*#####=+= ", + " #***#*###########++++= ", + " #***+ #########**#=== ", + " #***+ ###:::####=== ", + " **== ***::::***=== ", + " -- ***::: ****=== ", + " %%==++# %%%%#++- ", + " ***===== ####---- ", + " " + ], + [ + " ", + " %%%%%%%+==== ", + " ###++###=----- ", + " #######=---- ", + " #######=---- ", + " #######=---- ", + " %%%#===####-- %%%+++ ", + " %%%%#########*%%%%=-=+ ", + " +:=##****#######+==*- ", + " *##*****+*-*#*####+= ", + " ***#*****#%%%*######:== ", + " ##*#%*#=**###**##=-**###:= ", + " **#=+***###*++--######+ ", + " ***=*##-###*+*##**##***+ ", + " ***+* ###############*- ", + " #***+ **+#####****-- ", + " #***= ####-- ####-- ", + " ***== ****-- ****-- ", + " *- ****--- ****--- ", + " %%%#:=* #%%%%*=- ", + " ####:::: #####--- ", + " " + ], + [ + " ", + " %%%%%%%%%-- ", + " ###==####-== ", + " *########-= ", + " *########-= ", + " *########-= ", + " #%%*-- #####= %%%%# ", + " *%%*%%#%*********%###=* ", + " *%+*#*************** ", + " ********************** ", + " **** *******%%##*******- ", + " *# *%*** ***#**#**##-******* ", + " =**+ + ******=*++==******* ", + " ****-*#+ **********++***** ", + " ****+ :**************** ", + " #***+- ###**********# ", + " #**+- *****= *****- ", + " **+-# *****+ ****- ", + " :: ****+= *****- ", + " %%%#-- *%%%#* ", + " *####-- *****= ", + " " + ], + [ + " ", + " %%%%%%%%+ ", + " #***==**** ", + " *********: ", + " *********: ", + " *********: ", + " ##%%+ ***** ###+* ", + " =%##%#+##%%++++++++##- ", + " -%-*###%++++++++++++++ ", + " %+++++##+++++++*+++++++++ ", + " %+++++ ##*+++++%%%*+++++++ ", + " %+ %#-++ #*+++**%*****-++++++ ", + " ++* - =+++++==*==++++++++ ", + " #+++=+++ *#++++++++++==+++++ ", + " ++++= ##-+++++**++++++++ ", + " #+++=: ****-+++*++++++ ", + " ##+++: ***** ***** ", + " **++: ***** **** ", + " *-- **** ***** ", + " %%%*= #*%%## ", + " *****= +*++++ ", + " " + ], + [ + " ", + " ##%######## ", + " **#++++--+++ ", + " **++++++++= ", + " **++++++++= ", + " **++++++++= ", + " %%%#### *+++++%%%###* ", + " =+*#####*#%##%=+====== ", + " %%*###****%=+========== ", + " %%====*#**##=============== ", + " %===== --++#======%%%*====+= ", + " #= %*-== ++##+==++%+++++===+= ", + " ==+%- -%#++=====+=+--*===== ", + " #+==-== #%**++======+======== ", + " %=+==- #*+**+-====++======%= ", + " #+==- **+*++++:===+===%== ", + " ##+=-- *+++++ +++++ ", + " *+==- *++++ ++++++ ", + " *= *+++++ ++++++ ", + " +%#%%** %*+### ", + " +#***** ***++++ ", + " " + ], + [ + " ", + " %%%%%******* ", + " *****#===::=== ", + " ****#======= ", + " ****#======= ", + " ****#======= ", + " %%%%%=****====%%%**% ", + " ++++*****+*%**%%%=----- ", + " %%%=**#+++++%%--------=- ", + " %%----*#++++%%-----------= ", + " %%%----===+==%%%----%##----*- ", + " #-%%+:- **+==%%=-*==#+=*=---- ", + " %-- = +=%%*==++-----==::----- ", + " ==---- #%%*++++++---%---:---% ", + " %==-: +%%#=++=+:-----=------- ", + " *=--= ###=++====::--=----- ", + " **=-: ##==== ++==== ", + " ++=- ##==== ++==== ", + " += ##==== +++==== ", + " *%%####+ #%%*=**# ", + " ***+++++ ****==== ", + " " + ], + [ + " ", + " %%%%%%%++++++ ", + " *#######------- ", + " #####*------ ", + " #####*------ ", + " #####*------ ", + " %%%%%%++#----%%++- ", + " =++**+*++++%++*%%%%:::: ", + " =%%%:=++===+=-%%%--:::=: ", + " %%%:::**====-%%%------:-: ", + " %%%:::===+---:%%:--%##:::: ", + " %*:%=-: ***+---%%%:+:*#---:: ", + " --:%-+ %%%*----++:::=-==::: ", + " %:-:::+%%%+=====+++::-::-::: ", + " +-:- =+%%%-==-==::::-:-:::: ", + " +-:- ####-==-----::---:%- ", + " +-- ###----***--- ", + " =-- ###--- ***--- ", + " ++ ####--- ***--- ", + " %%%%###=%%%%#-** ", + " ####====#####-:: ", + " " + ], + [ + " ", + " %%%%%%%%==== ", + " *#######*--#-- ", + " *######*---- ", + " *######*---- ", + " *######*---- ", + " %%%%%%%=#--%%= ", + " =+++**=+=+=%=%%=%%%-= ", + " - %%%====--::*%%%==== ", + " %%%==+*+--:::%%%====- ", + " %%%%=====-----%%%=%**+ ", + " *%+#==***+-----%%%-**:- ", + " %== *%%%%--:--+++=-::# ", + " %-==-%%%+------+++=-:== ", + " --=*%%%%-*-::--:--=-=% ", + " =- %%%%-#:-:::::::=== ", + " =- %%%%--**--- ", + " =- ####---**--- ", + " = ####--***--- ", + " #%%%%#**%%%#-* ", + " #####+--####-- ", + " " + ], + [ + " ", + " %%%%%%%%#-- ", + " #########+-*= ", + " ########+== ", + " ########+== ", + " ########+== ", + " %%%%%%%%-%+ ", + " *+++++++=---# %%% ", + " - -+%%%+-=--:- %%% ", + " %%%%+*=--+* ### ", + " %%%%=====+%%####+ ", + " #%+=**==+==*####+= ", + " *=--+%%%#====-=+++-* ", + " #%++%%%*--= +++ ", + " -*-#%%%=#--= --- ", + " %=####=*--- --- ", + " #- ####=*== ", + " :+#####=+== ", + " : ####==+= ", + " ##%%%%*+%*= ", + " ######--**= ", + " " + ] + ] + }, + { + "width": 40, + "height": 10, + "frames": [ + [ + " ", + " **** ", + " ***= ", + " #+++- # ", + " *###-+# ", + " *-=-+= ", + " ##-%*- ", + " ## ", + " *** ", + " " + ], + [ + " ", + " *++++ ", + " *#++ ", + " ##%===*# ", + " ++*#**** ", + " -:+*=-*- ", + " =**%*= ", + " =**# ", + " *+++ ", + " " + ], + [ + " ", + " **==== ", + " **== ", + " #+#%#--# ", + " -+*=#**+ ", + " =*===*=- ", + " +*-++*#+ ", + " #-*+ # ", + " *==+++ ", + " " + ], + [ + " ", + " ###--- ", + " ###-- ", + " %%%%%%+-- ", + " =+**=##+ ", + " -#%*-#*+ ", + " ##:#=-*#% ", + " #::#= # ", + " #--*== ", + " " + ], + [ + " ", + " ###=- ", + " ###=- ", + " %%=%%%%+= ", + " =+***-## ", + " #-##=-##%* ", + " ##-#-* ## ", + " #- ##- ## ", + " ##- ##: ", + " " + ], + [ + " ", + " ####= ", + " *###= ", + " %#%#%%%%- ", + " *+***=-# ", + " *-%##*=* # ", + " *+#*## ## ", + " ## ##- #* ", + " **= #+ ", + " " + ], + [ + " ", + " **** ", + " **** ", + " ####%%%% ", + " +===+++ ** ", + " +*+#*** #%: ", + " :+**++* ** ", + " ** ** ** ", + " +** ** ", + " " + ], + [ + " ", + " *++++ ", + " *+++ ", + " =*=*##### ", + " ===+====-+ ", + " +=-=**++# *+* ", + " -==+++++ ** ", + " *++ ++ -* ", + " *+ +** ", + " " + ], + [ + " ", + " **===: ", + " **=== ", + " %+=++%##+ ", + " %%--=----= ", + " +---***==++== ", + " ---#==+= += ", + " #= +=+ == ", + " ##= *++ ", + " " + ], + [ + " ", + " ###-- ", + " ##--- ", + " %++=+#**+ ", + " %-%%:+:::- ", + " +===*#*-==# ", + " ::-#-*-- -- ", + " #-**- - ", + " ##-##== ", + " " + ], + [ + " ", + " ###*-- ", + " ##*- ", + " %-+++-#%= ", + " ##%%-==-- ", + " ++==*##-# ", + " --*%%=: : ", + " #:*: : ", + " ##+#*- ", + " " + ], + [ + " ", + " ####= ", + " ###+= ", + " % ++++ ", + " #%%%=- ", + " +++=*#=# ", + " : *##*- ", + " ##+ ", + " #**#- ", + " " + ], + [ + " ", + " **** ", + " #*** ", + " % %*==- ", + " ***#%%= ", + " *-=+** ", + " %*-+## ", + " ## ", + " **- ", + " " + ], + [ + " ", + " #*+++ ", + " #++- ", + " **#%%--- ", + " ****+##- ", + " *=-*-*+- ", + " #*-=** ", + " +=+* ", + " *+++ ", + " " + ], + [ + " ", + " ***=== ", + " **== ", + " ###%%*:: ", + " ##=##=** ", + " #++##:== ", + " ####=++- ", + " * *-*+ ", + " *+#*== ", + " " + ], + [ + " ", + " ###+-- ", + " ##+-- ", + " %###%-%-- ", + " #####-++ ", + " ##-*-##= ", + " #*#####*= ", + " * *:**= ", + " *==##- ", + " " + ], + [ + " ", + " #*#-- ", + " ###-- ", + " %%###%%%= ", + " *****##+ ", + " *+#*#+-##+ ", + " ** ###### ", + " *+ **- *- ", + " #:: ##- ", + " " + ], + [ + " ", + " ####= ", + " ###-= ", + " %%%***%#+ ", + " ******** ", + " # ****:*** ", + " *+ ****** ", + " *+ **+ ** ", + " #+ **= ", + " " + ], + [ + " ", + " **-* ", + " **** ", + " #%##+++* ", + " ++ #++++++ ", + " +=* ++=*=++ ", + " ++ *++++++ ", + " +: ** ** ", + " ** +*: ", + " " + ], + [ + " ", + " *++*+ ", + " #+++ ", + " +#####==* ", + " ==**====== ", + " --+ *+==++*== ", + " == *+:=+=== ", + " +- ++ +++ ", + " +** #+ ", + " " + ], + [ + " ", + " +*#=+= ", + " *#=== ", + " +*#**#%-- ", + " %-+++--*-- ", + " --=**=+--+:-- ", + " *- #++----- ", + " == #== += ", + " *++ *== ", + " " + ], + [ + " ", + " ###-- ", + " ##*-- ", + " =*+++**%: ", + " %:*==%-+:: ", + " --##--+:-=% ", + " :- ##==::-: ", + " - #--*- ", + " ##==##- ", + " " + ], + [ + " ", + " ###*-- ", + " ##*- ", + " =**==+=%= ", + " %%*-:%%=- ", + " =##:-+-:- ", + " = %%-::-= ", + " - #-*- ", + " ##--#- ", + " " + ], + [ + " ", + " ####= ", + " ###+= ", + " +++- % ", + " %+-*%# ", + " ==#===+= ", + " %##:- - ", + " ##+ ", + " ###-= ", + " " + ] + ] + }, + { + "width": 28, + "height": 5, + "frames": [ + [ + " ", + " * ", + " ** ", + " # ", + " " + ], + [ + " ", + " + ", + " ** ", + " * ", + " " + ], + [ + " ", + " *= ", + " =-- ", + " #+ ", + " " + ], + [ + " ", + " #- ", + " =:= ", + " ## # ", + " " + ], + [ + " ", + " #- ", + " =-# ", + " ## ", + " " + ], + [ + " ", + " ## ", + " :== ", + " ## * ", + " " + ], + [ + " ", + " * ", + " -+- ", + " * * ", + " " + ], + [ + " ", + " + ", + " %=- ", + " + * * ", + " " + ], + [ + " ", + " = ", + " %-: ", + " # + = ", + " " + ], + [ + " ", + " #- ", + " %:== ", + " -- ", + " " + ], + [ + " ", + " #- ", + " %-+ ", + " #:: ", + " " + ], + [ + " ", + " ## ", + " #%+ ", + " * ", + " " + ], + [ + " ", + " * ", + " *+ ", + " # ", + " " + ], + [ + " ", + " + ", + " %# ", + " + ", + " " + ], + [ + " ", + " *= ", + " %#* ", + " -+ ", + " " + ], + [ + " ", + " #- ", + " #*- ", + " * := ", + " " + ], + [ + " ", + " #- ", + " =%# ", + " -* ", + " " + ], + [ + " ", + " #= ", + " *+- ", + " * +* ", + " " + ], + [ + " ", + " * ", + " +#+ ", + " + * ", + " " + ], + [ + " ", + " + ", + " +%+ ", + " = * + ", + " " + ], + [ + " ", + " = ", + " %%- ", + " + # = ", + " " + ], + [ + " ", + " #- ", + " *-%: ", + " #* ", + " " + ], + [ + " ", + " #- ", + " =-% ", + " =#* ", + " " + ], + [ + " ", + " #= ", + " *+* ", + " # ", + " " + ] + ] + } + ] +} diff --git a/assets/ascii/paisaje.json b/assets/ascii/paisaje.json new file mode 100644 index 0000000..8fd3e20 --- /dev/null +++ b/assets/ascii/paisaje.json @@ -0,0 +1,1057 @@ +{ + "source": "paisaje.glb", + "frameMs": 125, + "variants": [ + { + "width": 64, + "height": 22, + "frames": [ + [ + " ", + " ", + " ", + " ", + " = =# ", + " ##+- ", + " %##-%%# ", + " #+##+*# ", + " *-***#=*+ %% % ", + " *==*+++-+ = %%+=++*#%-%= ", + " ####+++-=# **#=-#+*=:-- ", + " #*#*-*#%#- %-#++=*-=*% ", + " ******+-*=* %%+++=+--#%%+%-+++-##+- ", + " +****%*+########**:*+##:#++*-=#+%**+- ", + " +**#****#####**#**:**-+:%++#-+:%%#*=- ", + " +**%#==-:###**##**-**++#+:=#-*=%#*%#% ", + " ####%%%###%##=%%#*####%%**#%#+++=%+-%#-#+**%* ", + " *****########=####**********+*#*++#*#*==-----= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " * #=# ", + " ***= ", + " %%#**## ", + " ##%=##+* ", + " ++*-+**+#-+ *%%% ", + " +*+*==: = %%%+-++%-# ", + " +**#==+ #*+=-*=+-+- ", + " ##*#==*#%-+# =%% #: %=:+*-+-% ", + " **++#+++##*==#- #:%==++ =*=:%%=%:*- ", + " ***+****#**=-%%=*****#*+=+===*%%*==+:+% ", + " *#*+****#**=-**%*******+=+====:-+#=#:-%# ", + " *#*****%%***-===#*******=+=====%%-+*:+ ", + " *%%%%%%%%%%%#*##%+##-*#***%%%++###==-**#+-+%+ ", + " *****#############*#*#*****+++++++===###+****-= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " #* *# ", + " ##**= ", + " %%%%*** ", + " *##%+-#* ", + " =+**+-##+*#*-= ##* ", + " +=***+-- -+ %%%=:#+ ", + " -+*+*#** -%+=:---- ", + " ***####*-=**#*#:**#- **%%=#-++-= ", + " +****%%*==#===-+##+-==-= +%%--%+**##=++-%-# ", + " ****#%*********#++-++++%-++*#++=----==-%**-=% ", + " ****##%*********#++-+++#*+++*#++=---:-=-===*#+* ", + " ****##=********%%++-++:+=-+:*#++=--::---%#==- ", + " ###%%%%%%%%%%%%%%#***%=#%#*#++#%##=**+:%**++*+ ", + " *****############**********+#-+++=====----:###=+++ ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " *+ #- ", + " ##++: ", + " #%%%%++ ", + " %#%%#%%:%* ", + " =****=--#%+***- ** ", + " +*+***=+: *:- %%%%=+ ", + " =-+**=*+#+ *%+-=+ ", + " #++####*+*--=# #+#-++ =+%%:+ #*=:- ", + " ******++%%%%##----=: *##=-+*###-+-%#:*#*:- ===#% ", + " **+**++%%%%##***##****##=+======%%#=-:--:- ==*= ", + " ***#*+%%%%%####*##***###++=====#*#%=-:--:- +=-- ", + " ***##+%%%+%######****%%%++====-+=-#*=:--:- %*#+ ", + " ##%%%%%%%%%%%%%%%##%%%%##++%#*##*+**#%**+-%+= *-* ", + " +***#############***********++++++*==*=-------= ###== ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " +=+#* ", + " +#*== ", + " %#%%%%% ", + " %###%%##%-% ", + " =-****+---*%=+++- *** ", + " +******+-== *= -*%%%##- ", + " #---****-=*=*= ==*=%=== ", + " #==%####%**+= ####= #==#%= ==%++ ", + " +***-+###=#%%%%%###-#-#*+**#**-- #+##-*= =+#+#=: ", + " +#=*=####=#*%%%%###******+++**--------*### ##**+*% ", + " +#*######=##*%%%####****++++**--------*##=% ==+#*+= ", + " +#*##########**%######*++++#%%-------#==--- #%#+- ", + " %####%%%%%%%%%%%%%%%%%%########==*+*-%%=+%+*-* +=+# ", + " ****###########************++++++====---+-*--= ###*#* ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " * =#* ", + " *#*-- ", + " %%##%%## ", + " #%#%%###*##: ", + " %=-****+==--*%-=== # **+ ", + " #%++****++:: #- * -%#%+%%#** ", + " %-= **+++:- *-- **=*#+##--+ ", + " % #=*%-##-%######-*+= %=**#-=-*#= *-++=:%= ", + " *++++*:#***--%%%%%%%***-# #*****%%+** ++=##%-+#- ", + " ##--#-#***-##*%%%%%*********===+**:::::+- %**###+*+#++ ", + " ##*######*-###**%%%********====#+*::::-*- *#%===:*=---- ", + " ##*#######*####**%%*******====####:::--*-%-+: =##+%#=+ ", + " #%%%%%%%%%%%%%%%%%###########******:-*=+%-*#-=+= +=-== ", + " +***#############*********+++++++++====---::-#=*+ ##:+#**+ ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " *#*+ ", + " **+- ", + " %%#**### ", + " %%*%#######* ", + " %==-***++==--+%--+ % ** ", + " #==== ++*++-- # + *#%+%%+%%%#+= ", + " ##:= =***++-- +*- %**+=*#+###-+ ", + " ** -## + #-%%****#++*= *#%:*+* %==+++-*+# ", + " #+====+ ***-########*** # **+#++=%++#=+- #*#*=++: ", + " #####:-*-*****-**######*******++---++==== ##:#%%++=-#=# ", + " ######********-****####*******++---=+-=-= -*+#* ##+%:= ", + " ######****************#*******+---*#*-=-+ %%--=-*- ", + " ##%##%%%%%%%%%%%%%%%%%%*****##***++++++#=%+* =#--=- ", + " *****##############*********++++++======-----= *#-***++ ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " +-#: ", + " **+= ", + " #%%***#* ", + " %%%#%##**=** ", + " %==*+++==:+=-#:: +: ", + " %#%=- ++++=== *= =##++%#-%%#*= ", + " %%#+ +++**=== ***= #*+*-#**=#+*= ", + " + *=#: - %%**##++++*%**-*=** %%#+=+ +++==#:* ", + " -*####*+=--+-- *+++********++++#-- **=== ####+===+- ", + " #*#**#####*** **++++*******+++++++==---=-+##-#*-%%=-= ", + " #+#**##*###*****++++++*****+++++++==:-+=-+=-=*++-*%-+ ", + " #+***##*###*****+++++++****+++++++=::-+*=### #%-*+ ", + " %%#*#%####%%%%%%%%%###########++++*****====--- +=+-:: ", + " *****#############*********+++++++===-----::-== *+#-- ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " =#*#= ", + " **+=: ", + " %##*+*+ ", + " %%###**++*+= ", + " %=+====--*+:*- = *= ", + " %#+::**===-- %#++=##=#%%=##%#+ ", + " %%%#= **+++-- ##++#+=#**==+*##+ ", + " =# =** % - %%%#++#====###+=+= %%%*== +** ", + " --*##%**:+#*=+:- #**===********===%%%#**-+ ==* ", + " ##******####**# *+#++====+******======--=-*- #*+%% ", + " ##******####****+++++=====++****======--=:++= %= ## ", + " ##******####+***+++++======+++**======--=+%#- %%=+ ", + " ###%%%**%%#*##*%%%########**********===++==--- #* ", + " *****############*********++++++======---::--=-- ## ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " = +* ", + " ##=- ", + " #%*#+-+ ", + " %%*-*++=-+= ", + " %==**--::*-*- ", + " #*%-***----:=%==*%*#+ ", + " ###**+**===--###===#-*+ ", + " #% %#* #---%%%##++---=##==++%%*#- ", + " *:**#+%%%+# **=+=**#####--=====##-- ##+- ", + " *#*#***#++####*#*++*-#=#=#--=====##------- ## ", + " *####**#++####***+=***====---=====#--:--+- #* ", + " ########++####***==***====----====#--::-+% %*+ ", + " %**%%%%*%=+%%%%#**+%%%%########++++++++--=--- # # ", + " +***#############***********+++++++====-------= * ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " #- =* ", + " ###== ", + " %%#%+*=- ", + " #%*+**=-= ", + " ##=-*::== ", + " %=-*#++=**::=+ %- ", + " #*###+****--=###- ", + " *+-##- #+-%# #-=%%%%###---##-%%#+= ", + " -**--+*- %%%===+++=++-+#######:---##--== ", + " ####*#######=#####*#**=+*#-#-#:---##---= ## ", + " #*#**#######=#####*#**=+++----:----#---= *# ", + " *#*#**#######=#####*#**-+++----:---------**++ ", + " %%%%++%%%%%%##-%%%%%%%++=%%##*******======:-:* ", + " *****##########************++++++====-----:--=+ ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " #**#+ ", + " ##*=: ", + " %%#%%++ ", + " *%##=+-- ", + " % *= ##*=-*-- ", + " %%--*%:**:-%+-=*=***-+ %- ", + " ##-+-+**=--+-***=***-+**- ", + " #+*=%#*%-##====--* +--=#%%*##-**--- ", + " #+**+%##-=+##==%###-+# %%*=+-=*####=### ", + " ###+-#**+#####*##-******##+--=%--#=:##= ", + " ###+*##++#####*##-******##++:==---=:##= ", + " *#++*##++#####*##-******##++:==---=::== ", + " %%%#*%%##=%%%%%%%###%%%%%%#*#=-%%##+++++----- ", + " +***#############*********+++++++++====---::-== ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " #* % ", + " ##+- ", + " =%##%%- ", + " *%%**-- ", + " % +: --##%+-=- ", + " %%+*=#-+%+#= # -#=+-+=#= ", + " *#+++-----=: *=+++++=+= ", + " #+*=+##**=+ ---#%*###- ", + " #+++==#=%##--=+::%#%##* =%-*-==**** ", + " *###=-**##*==************+++#**=--=** ", + " *##*=-**##*==***********++++##*==--=* ", + " ##**=-%%##*==***********++++###==--=* ", + " #%%%#%#%*#%##-#########**%####**%#-%%**====-: ", + " *****#############**********++++++======-----= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " +** % ", + " #*++ ", + " =##**%% ", + " ++%%%%+- ", + " %%=- %-*#***%%=+ ", + " %%%=+*=:#- = -#+*=:- ", + " -+%#--+---- **===== ", + " %%++=*-** #= #%+ *##%---#+### ", + " *++=#%++#-- -%###:- #%**+=-+++-#+-- ", + " #+**#***--====###+-******#++=+++++#**-: ", + " +::+=##*+--===+##++-******#+*=+++++#**-- ", + " +%#*#++-%***+##++-******#**=+++++#**-- ", + " %#%%%%%%#-*+%###%%*:*####%*##++####%*-###++:- ", + " +****#############*********+++++++====-----:--= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " += +% ", + " *#*== ", + " =###*%% ", + " -=%%#%%= ", + " %%- -:**#%+**++%-* ", + " %%%=*=# += +:#++*=- ", + " =+%*==-=- *+*+==:= ", + " *%=*=**-#:+ *=-%#=#%+#=%%-===*** ", + " *%=*#****=+=#===+= **%**+-==+=##*++==##-#=+ ", + " %=-+=-###--##*++-==++++##*==++++++#====---*++ ", + " :+----=*##=-##*+=-==%++%##*==++++++#=+==---*++= ", + " ##+*=*#%####*+=:#++:%+##*==++++++#++==---*++= ", + " *-+%%#%%#%#%%**#%+%%#**%%+-****%%*##=***%+:**- ", + " #+++*****############*********+++++++=====---::--- ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " ** =# ", + " *##+: ", + " --**+%# ", + " %=-%%%%%%% ", + " %# -=**%:%%=++=*%% ", + " #%%##= -*- ****==%*- ", + " =++%+- ##=+*=+#-- ", + " *+*%- +%+##= +#-%%* =+==#+%%=-=+++ ", + " %+##= **++++%%%=++=*--****: -#%+#*%--##+==*+*+#= ", + " %%=- *###:-:**-*##====***:##*#++-=====##--::*== ", + " *=:= *###--:+*-*##*=-*#--#=###++-=====##=-::*== ", + " =*#- *###*--#+-*##*--=-%:#####++-=====#-=-::*== ", + " =+= %%#%%%%#%%#####%%+*%%#%%#++%%=++++%%**-+%%-*: ", + " ##==- ****############************++++++====--------= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " **##* ", + " ***=- ", + " -:++=** ", + " -=::%#%%%## ", + " %%- %-*+%%-=%%===-*#% ", + " %#%%%#*- -- +**-**-=*%+ ", + " +#+#+#-= ###+#-++-=*#-- ", + " *++## *=%+#= *%### %%--##==-##+= ", + " *#%*=-= ###===- *+++*==+--#*=---*-##=*###*#-**=# ", + " %*#%%*- #***+------+++###+-###-+:+*##*=----**:-+-- ", + " -#*#=++ %#***=------+%####+-###===:*##*=----**:-+-- ", + " -=++= =##*#*=----#-*####+:#%%%###*#*==----**:-+-- ", + " =--+ +##%####+%%%%*%%#%#**%#-+%##%%##=%#-==%%**-%:+ ", + " ####-- +****###########***********++++++====-----:--= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " *** : ", + " #**+- ", + " #%--=+%* ", + " --%:-%*#%%#* ", + " %%+ * +*-=%#==+%%=---*## ", + " %=*#%%%**- * *- *+**=+-**%%= ", + " ==*##++#**- ##* *+#-=+- *#: ", + " -**-+=*# %*%=***%### ##%%::#+%%%--*=-#** % ", + " +###%*#--* %%+**--#=== ###*====-++-:##***-+***-* ", + " %-##%+%*+-%* #*#+++--===****######**--##-::+##**---+== ", + " #=*#=##+++#*# #*#*++:-==#+*#####%##**==##%#=:##**---+=- ", + " +#*++=-* =-%-#*#*+++-###::##%*#%%#**==########**---+-- ", + " =:=-- #==##%#*##+%#*=#%%%%%+###%*++##:#%#####-+##%%++- ", + " #***##:- *-+****#############*********++++++++====---:::-= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " +**# ", + " #**= ", + " %%%--==+ ", + " #--%%-#%*%#* ", + " *# * +%--#==++%%=-:=*** ", + " ##=#%*%%%%*+= - * +*++*+= ***%= ", + " #=#*%##-*#*++ #** =*+**-== :**- ", + " -*#*=*=-*# ##*-#-+ ***%%%=#++%%% * +*= -* ", + " #+-#%%#+ #*+=%*=+*%**+: * ****=-+==*#+--+ *#*+==+ ", + " %%=###-+--%%+ #**#*#*==++********:-=***#+++**=####**+=+ ", + " %+%+## #*### #+***#*==+*##*-**%#%*****#++-*######**+== ", + " +#=+===: #+***#**==+**=***%+%%#-+%#++-#**####*++== ", + " ==#*=- *##%*%%%+*#+*##*-*%###%=**##%+==*%%%**%**#*- ", + " *+++*##- *****#############*********+++++++======-----= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " =++# ", + " #++- ", + " %%%#-=+= ", + " %--%%%-%=%#+ ", + " %% +%=-*+*#+##===*+ ", + " %%*###+#%%#*= +- ***=++* ***%- ", + " +#-%%%##-##*= %*++ +*==++-+ -**= ", + " ::+#+*-* -##=#* +#*##:++#%%%+*=*### # #*=- % ", + " #--+%#%#*- *#--= **%++=++#*-+===++++ #*#+::*#*%+==- ", + " #*-%%%##%#+=%%-*###**#+--++*=-:=++#--=# ###==**#*##++ ", + " ##%*#=##+#+:+#=*###**#+--+****##*+%%%***##*==*##*##+= ", + " =-+-# ==-+#**###**#++-****#*%-*=*+%*#%%*==-##*#**= ", + " -%-+== ===##%%%#####++%#+%##++++%*###-+*%%=-+%%%+%+*+ ", + " ##+== **+**#############**********++++++====-----:--= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " #-%=* ", + " +#==- ", + " #####== ", + " *%--=%#-*-#* ", + " %* % %+#%**##+#*-+* ", + " %%##%*%=#%#%*-+++ ***+-=--*+#- ", + " *+:#%%#**+#***%+= *++-#=- +-#** ", + " *-- ***+-# =#**#=**++%-%++##* % # ##- -# ", + " ##= %##*+%%=#+-#=*##-=-==+++* =+#++%%#=--*%=-- ", + " %%%** +%##%#-*#%*#####**::=+--==++ #######*+-#**#- ", + " *# -% ##-##+**-#=-#####**-:=**%#++#++######*+-#*#+- ", + " *=** =+*#**+-:########**=-*+%%*##+#+*%%%%%++-=##+- ", + " #* --%%=++%%%%###%%##=%##*%##====%****=+##-=%%==- ", + " #- ##--*****+#########*********+++++++======---:--- ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " *: - ", + " #*-- ", + " ##*#%%+ ", + " *%%:+%%#=*+ ", + " %-+=*###+**-+ ", + " -###=**-%#+*#**:-:++# ", + " #+#-####++-*++-#-==+##* ", + " %+#+=+=*#*++%:=:***+%+#%=# *** ## ", + " **-+ %=***=****=+***#-:***=: %#####+*-=- ", + " ## *+***+%**##-####*##++-**+#######***###+:*: ", + " %* *-**##-***+*##%#*##++-*%*=######*#*###=:*: ", + " ++# *#**==:*#*+:-###*##++-*##=+###%#*%%#%%=:=: ", + " # + #%%#%%=:##+%=+%###%%%%*:%%#+%%##--%%+++-=##%: ", + " * ****###:#######-************++++++====--------= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " ** #: ", + " ###+: ", + " %#+#%%-+ ", + " +%%-##%== ", + " +*-#-#%#- ", + " #- %####%#*#+-%*+ ", + " %%%**#=*##*+*=+== ", + " %=+#+=*#%#+%++*++-%=+ *=#=# +*+=#= ", + " ##*==##****#%%+++==%+-+**-%%**# +*###++: ", + " ## ##*###-*=#**#-++#=**%##*=*##+########**= ", + " ** ##*####+:###-#++*=***%**=-##+########**- ", + " -***##*###**##*=-#*#*=-**#**=-*##%###%%#%%%-- ", + " ***%%%%%%%%%%%+=%%%%+***#%%%++%%#:%##:%%%-=-** ", + " +****##########*****###-****++++++====-----:--= ", + " ", + " ", + " ", + " " + ], + [ + " ", + " ", + " ", + " ", + " *++-- ", + " ##*=- ", + " %#==%%* ", + " +%%=###+ ", + " +**=:=*= %% % ", + " *- ++***=+-%%=+*#%#%%-%== ", + " #%%%***--+-+**++#**+=-++= ", + " %%==#-=%%%#=-=%- ++++=:#-=##-++#=#+ ", + " +#**-=*##+%*##++ %*%=++##=:+=**+#%#**== ", + " %*=**########-%#+#+**=-**=%+-*##*-*#**- ", + " *%#*+#########=--%***==*-*%%-*#**=*#*+- ", + " =-=*+#########--#*-:*+**-**--*#*%%*#%%: ", + " *##***#%#%%#####%%#-%++%#*+-#%%##=##+##%%%#:+ ", + " +****=++##########****#=***###-++++====---:::== ", + " ", + " ", + " ", + " " + ] + ] + }, + { + "width": 40, + "height": 10, + "frames": [ + [ + " ", + " = ", + " %+%% ", + " +**#: ", + " #+%%*# %#*#*:: ", + " *##*=+ %#%+ =+*%+-# ", + " ***-####***#%%#+:=+- ", + " *%%%##*#*%*+#:*+*#%% ", + " +**########****+**+##=---= ", + " " + ], + [ + " ", + " -* ", + " ##*# ", + " +-#*=# ", + " =*# - %*=+:: ", + " #+++*## %%# %-==#+ ", + " *+**#*-#***#++==%-=:# ", + " #**%%#-%#***%+==#-** ", + " **#######**-**++++==*-**= ", + " " + ], + [ + " ", + " #*: ", + " ###* ", + " #*-###* ", + " -%*-+ %*== ", + " %***===-*%*- #%*+#%%#-- ", + " **#*****#+++#=+#+=--=###+ ", + " *##****%%*++%-*##=-:-%-- ", + " ***######******#++===-:##-+ ", + " " + ], + [ + " ", + " *+* ", + " #%*+ ", + " %%*-#%** ", + " -%%*: ##= ", + " *%*++###--: ##+- *:#*#* #- ", + " ***+%%##*#**#=-===*=--: #** ", + " **#+%%####**%+===#=*--- %*= ", + " **#######******+++*==---- ##- ", + " " + ], + [ + " ", + " *# ", + " #%* ", + " %#**-**++ ", + " #-%%%#= + ***#- ", + " %## #=####:- # *#= #*=* +=# ", + " *###=#%%##***++*----*+ +%* ", + " *######*###*++%%---==#- ++* ", + " **######******++++==-#:-= #*# ", + " " + ], + [ + " ", + " == ", + " %#% ", + " %%%*==*=== ", + " %-%%%* - + =#%**+ ", + " =#+*##%###** =*%++* *#*=* ", + " #*##**#*%%*****==*:::+ *=#+#-+ ", + " #*###*##******==##::-*-##++#+ ", + " +**#######*****++++==--:-=+ -#* ", + " " + ], + [ + " ", + " *#+ ", + " %#* ", + " =*##+=-+-- ", + " %: %%%*+*+ #-%#%++ ", + " #%## ##%***** *=+#+%- #-** ", + " ###*******##****+--+=+ =##*+== ", + " ###*************+-*#=+ -%-+ ", + " ***#######*****++++===--- *-=++ ", + " " + ], + [ + " ", + " +#= ", + " %#* ", + " =*##=+-#- ", + " %#-%%%# ** ###%*%+= ", + " %+:%*%++#*#+++*+#* +== #+ ++ ", + " ##*###***++****+++=:-=#*###- ", + " #***##***+++***++++:+*#- #%- ", + " ***#######*****+++===--:- *#- ", + " " + ], + [ + " ", + " + ", + " ##+ ", + " =##*-*:- ", + " %#=%%%# #+#*##+#%= ", + " #%*#%*%* =%*+===*==%#-= +# ", + " #***#****++===***===-=-= +#= ", + " #***#%+**++====+*===-=+# *=% ", + " ***######*****+++====-:-=- #- ", + " " + ], + [ + " ", + " =# ", + " %%#- ", + " =-+:** ", + " *=*%%#**#=+*- ", + " **+% #%+-%*++--#--+-- ", + " ##*#+###*=*==--===---+ * ", + " ####+##**=*===--==-::- *+ ", + " **#######******++++==---- * ", + " " + ], + [ + " ", + " -= ", + " #%*- ", + " =+=-+ ", + " ###*%*+## ", + " ##++= % **+=#%##=--#--= ", + " ##*####=##*#=++----#-- + ", + " ***####=##*%-++-------%+ ", + " **######******++++==----= ", + " " + ], + [ + " ", + " #*- ", + " %#%+ ", + " #=-=+ ", + " =%+-+=:#*=%*** ", + " #%##==#+%=+ #%#+##:#= ", + " ##-#+####-***#+-=-=#= ", + " *+*#+####-***##:=-=:= ", + " **#######*****++++==--:-= ", + " " + ], + [ + " ", + " * ", + " %##% ", + " -#=-- ", + " #++==:* #+=%+= ", + " #*##*#- +*#% #####- ", + " *#=**#=******++#*--= ", + " %*=%**=******++##=-= ", + " +**########****++++===---= ", + " " + ], + [ + " ", + " +# ", + " ##*% ", + " +=*-=+ ", + " =##--- + *%= ", + " *##*#: =#- *#%-#*# ", + " %%#*+-=%#+****+=++#*- ", + " %%#-%*+#+*****=++#*- ", + " ***#######*****+++==---:- ", + " " + ], + [ + " ", + " *+% ", + " ##*% ", + " *#+*--- ", + " =+-= %%*:= ", + " =--%*#++*= **#*++%#=-%+ ", + " **#-#=-#+-+++#*=+++#+=-*+ ", + " ++=#%%%=:+:=#*-+++#+=-*# ", + " *+***######*****++++===---- ", + " " + ], + [ + " ", + " **# ", + " **+# ", + " =+%*++-= ", + " ==* %%#+% ", + " ++ #+=%=* =*+* %-++%=#*+%* ", + " %=- ##::-*#==*:#*#-===--:*= ", + " -=: ##*%+##*#-+##+-====-:*= ", + " #=- **#######******+++==----= ", + " " + ], + [ + " ", + " +- ", + " :+= ", + " *+%%*=--+ ", + " :+=++ * #%%#+=- ", + " +## *#=# *+# # =--%%=#= *%+ ", + " +%* **+---+*#-##++*#=--*-+ ", + " -=* =#*+--#%%%+#%###*=--*-+ ", + " #=- +**######******+++==---:- ", + " " + ], + [ + " ", + " *# ", + " -=- ", + " +%-#==*--- ", + " %#%%=+ * # %%%+=- ", + " ##:%* +**-*= #%#++##%-+-% ", + " :#*%**= +#+-==**%##*=#+-*#*-+- ", + " +++-#--+%++#%:-*-%%+###%#*-+- ", + " **# #***#######*****++++==--:-= ", + " " + ], + [ + " ", + " +*- ", + " --- ", + " +%#**-*:== ", + " #%%%%+- ##*%%%# +- ", + " %*-- #-*+**+ **==+#*# *%=% ", + " *=%**++ #***=+***#+**#+-##**+= ", + " =-== #***=***:%#+%%%-*#%*+= ", + " #++#- **########*****+++===---= ", + " " + ], + [ + " ", + " ++# ", + " %=- ", + " +#*+#:*-= ", + " %=#%%%** *+ %%%+::# ", + " %+ #= %*- *+=+==%=*+=*-#%*%* ", + " *=+#%:%=##*#-+%+*+#=*#*=**#+ ", + " === ==#*##*%-*%-%**%*%*#%%%= ", + " #+= **#######*****++++==--:-= ", + " " + ], + [ + " ", + " = ", + " %#= ", + " %#=##-++ ", + " %%%%%##+=+ %%#*-+# ", + " %= #++#%-=*#-++#- +*+*%+*# ", + " #=- %#*+*%###*:=++++##*#+-*- ", + " *== +=*=*####%=*%#*#*=#%+:%+ ", + " #* #-***+#####****++++===---- ", + " " + ], + [ + " ", + " :- ", + " %#%+ ", + " %=%#+= ", + " %#*###-%#*+=# ", + " %#+##*-%#+-#-%%+ #*+= ", + " * :*--*##--#+-*####*+#+: ", + " %- #*-%++-###%-%:##-*%%#* ", + " * **%:###+#******+++==----= ", + " " + ], + [ + " ", + " *# ", + " %+%% ", + " +*+#+ ", + " #%#-##=%* ", + " %+#-%#--=:%#-*- % #+#*- ", + " + ###%*%#+***#*-######*= ", + " =-###*#+-##-***-####%%#* ", + " ***######*##-**+++==----- ", + " " + ], + [ + " ", + " ++- ", + " %*#% ", + " +**=: ", + " **%%#-##-**+:- ", + " #=#=%=%*# +*-%*#++#:# ", + " =#####*%*%*=+*=##-**- ", + " =+#####-*+*-#**##%*%* ", + " +***######**#*+##++==--:- ", + " " + ] + ] + }, + { + "width": 28, + "height": 5, + "frames": [ + [ + " ", + " %* ", + " += %= ", + " %%##*++% ", + " " + ], + [ + " ", + " * ", + " = -- ", + " #%%%%*+== ", + " " + ], + [ + " ", + " - ", + " * * # ", + " #%%%+%#:= ", + " " + ], + [ + " ", + " + ", + " #* * ", + " *%%#%=%-+- ", + " " + ], + [ + " ", + " % ", + " -%* - ", + " %##%%#%-# = ", + " " + ], + [ + " ", + " * ", + " *%* + ", + " #####*:-== ", + " " + ], + [ + " ", + " * ", + " = *+ ++ ", + " #**#*#+# * ", + " " + ], + [ + " ", + " ", + " +#** #+ ", + " *#*+#+*=-- ", + " " + ], + [ + " ", + " * ", + " #=--+ ", + " %#*+=#=+=+ ", + " " + ], + [ + " ", + " = ", + " %*# ", + " %%#+=-##-# ", + " " + ], + [ + " ", + " #: ", + " +%+- ", + " %#%#%+:#: ", + " " + ], + [ + " ", + " # ", + " +*+%* ", + " %%#%*%==# ", + " " + ], + [ + " ", + " *- ", + " =- +% ", + " %%***+-% ", + " " + ], + [ + " ", + " * ", + " =- = ", + " ##%#*%+%* ", + " " + ], + [ + " ", + " # ", + " = + % ", + " %##:#+#-# ", + " " + ], + [ + " ", + " * ", + " = *- ", + " =%*#%##=%* ", + " " + ], + [ + " ", + " + ", + " = #*: ", + " + #-%*%%**+ ", + " " + ], + [ + " ", + " - ", + " * %%= ", + " --*=:*+##- ", + " " + ], + [ + " ", + " % ", + " -+ ** = ", + " % **%#=%#% ", + " " + ], + [ + " ", + " ", + " #- +%-* ", + " =+**=*+-%* ", + " " + ], + [ + " ", + " * ", + " #**%* ", + " +=+#%***%+ ", + " " + ], + [ + " ", + " * ", + " -=+ ", + " #%=*#%##%# ", + " " + ], + [ + " ", + " %# ", + " **+- ", + " -#%#*#%#- ", + " " + ], + [ + " ", + " = ", + " *%%++ ", + " %*#-==%%* ", + " " + ] + ] + } + ], + "composedFrom": ["paisaje.glb", "heroe.glb", "coloso.glb"] +} diff --git a/assets/ascii/yelmo.json b/assets/ascii/yelmo.json new file mode 100644 index 0000000..2520f75 --- /dev/null +++ b/assets/ascii/yelmo.json @@ -0,0 +1,1056 @@ +{ + "source": "yelmo.glb", + "frameMs": 125, + "variants": [ + { + "width": 64, + "height": 22, + "frames": [ + [ + " ", + " ############# ", + " ######%%%%%%%###***** ", + " #####%%%%%%%%%%%%%%#****** ", + " ###-#%%%%%%%%%%%%%%%####+++=+** ", + " ##*###%%%%%%%%%%%%%%%######++==+## ", + " #**###%%%%%%%%%%%%%%#**###***+=**#*== ", + " **-## ##%%%%%%%%%%####***#****##*++== ", + " -#*-# ###**%%%%%*######**#**##++=:==-= ", + " *** +***************+#++++====--==- ", + " *** +***************+#++==%+++- *+- ", + " *** +**************++#+-+###== =+ ", + " ** +*****+++++==*****+=++-==**=-+ ", + " *** -==#************++++=+==---:= ", + " ** *##************+++++++====-=- ", + " - ***************#****-=====-=: ", + " ++#*###*****##****-=====-=: ", + " ++##***======-:: ", + " +====--:: ", + " ==--:: ", + " == ", + " " + ], + [ + " ", + " #######****** ", + " ******%%%%#####****+ ", + " *****%%%%%%%%%%######+++++ ", + " ******%%%%%%%%%%%%%%%%#***==+++ ", + " **+**%%%%%%%%%%%######******=-=** ", + " #*+=**%%%%%%%%#########********=#+*= ", + " %++=***%##%%%########*******#+++**++= ", + " -#+* *#####*######*********#++*==--+= ", + " ++ **************++++++=*-+==-----: ", + " +++ +*************+++++=*==-#===-*= ", + " +++ *+************++++==*=-=***-+#+ ", + " +++ ++*******++++--++++++--=::-+-% ", + " +++ -=====#****++++++++====*==---- ", + " =++ ***##*************======---=-= ", + " ******************#++++------- ", + " --*+++#*####******+++:----:-- ", + " +++++++**++:----:- ", + " *****+=---:- ", + " **##==-- ", + " ### = ", + " " + ], + [ + " ", + " #******+++++ ", + " %*****%%%%%####+++= ", + " +++++%%%%%%%%%%#####+== ", + " %++++*%%%%%%%%%%#######+++-= ", + " %+++++%%%%%%%%%%%##**##+++++-+ ", + " %+=+++%%%%%%#########**+++++==:+ ", + " #%==++*%%##%%##########++++++*===+= ", + " %-#=+ +%#####**######++++++++**=+--= ", + " *== =:*********+++++=+++++-*#---- ", + " *== ++*********++++++=+++-*+:-#--= ", + " === +++********++++++===+-*+=+++- ", + " +== ++********+=====--=====----:= ", + " +== -+-======#*****=======---=*+== ", + " +== +#######******++++++-----:--: ", + " * *######********+++++#====---+ ", + " *+-+--**++**+****=+++*===--:-+ ", + " ===++++***++++==-::-+ ", + " ++*****#**+--- ", + " *****#* == ", + " *## ++ ", + " " + ], + [ + " ", + " +++++===== ", + " %=====####***--- ", + " %====%%%%%%####****- ", + " %%==++%%%%%%%%####****== ", + " %%====%%%%%%%####**+***=== ", + " %%=-==%%%%%##*********+====- ", + " %%--=+%%#####*********=====+--= ", + " %%--=+%######+*******=======+-=- ", + " **-++=:***+++****++++=-====+=--= ", + " **-++=++**++****+++++=--==:*-=-- ", + " **-++=++***+***+++++===--=:*-== ", + " *--++++******++++---------:==-= ", + " +--*--=========#++++=-----::+#++ ", + " ++--**#########++++++=====:::=+ ", + " *=**########+++++++====*+::== ", + " ++=:++--+*+==**++++:==++::== ", + " ---==++++++****+++=:-= ", + " ==+++++***+ ++- ", + " ++****+ ++ ", + " **+ + ", + " " + ], + [ + " ", + " %==----- ", + " %%---####*:: ", + " %%---#########** ", + " %%---%%%%####***+++- ", + " %%---%%%%%%%****++=++- ", + " %%---##%%####**+++++=-:- ", + " *%%--%#######*+++++++--::- ", + " *%%--%%######++++++++----=-= ", + " +##-=--++++++*+++----::::+%+ ", + " +*#--***++++**+++=---:::-+== ", + " +**--***++++**+++==---::-+- ", + " +*-****+++***+++=::::#:--++ ", + " ++--:=====-----*===::::-=+* ", + " +++--###*#*##**=====:::--* ", + " +***######***=======:::*-- ", + " =*+=#--++==+**===++===::=-- ", + " +----=====+*+ ***- ", + " =-====++*+ *++ ", + " ==++*+ +*++ ", + " *+ *- ", + " " + ], + [ + " ", + " %::: ", + " %%::**+ ", + " %%%%-*####***+ ", + " %%%%%-:####****++=+ ", + " ##%%%%%=+###*##***+=-= ", + " *##%%%%%-####***++====-- ", + " ###%%%%--##*****+=====-- ", + " *###%#%%=%******======---- ", + " +*******::+++===+===::----% ", + " +*******+++++==++===::---- ", + " ++******+++++++++===-:--=- ", + " ++******+++++++++==--==--= ", + " +*#+=++===-----::+-----+ ", + " +*###++=##*****++------+ ", + " +*####**##******-------== ", + " ++++-:---++= ++**--==:---= ", + " ++-::---=+= *#**** ", + " ==:---=+= *#**=+ ", + " =-==+= *#*** ", + " =+ **# ", + " " + ], + [ + " ", + " %- ", + " %*%%** ", + " %%%%%*%%****+ ", + " %%%%%%%+%%****+++= ", + " #%%%%%%%%*%%******++=- ", + " ##%%%%%%%%-%%*****+---:: ", + " *#%%%%%%%%-%%**++++----: ", + " **#%%%%%##%%%++++++=---- ", + " ++**#####***-**+===------==+ ", + " ***###****++**+==--=--:-=: ", + " *********+++**+==-==-:--=: ", + " ++++***+++++++=====::-== ", + " +***#+----++-------=-- ", + " #****##****==*+++++=--- ", + " +****##*******+++++----- ", + " *****++- +++ ***--:-- ", + " ++---:-- ###****** ", + " -=----- ###***++ ", + " =:--= ###*- ", + " -= ## ", + " " + ], + [ + " ", + " *#### ", + " *%%+##%% ", + " %%%%%%%%**%#++ ", + " %%%%%%%%%%%%**#**++ ", + " %%%%%%%%%%%%%**##++++= ", + " *%%%%%%%%%%%%%+*##++==:- ", + " #*##%%%%%%######*%#*===:: ", + " #**####%%########*##===-:- ", + " +***#**######***---++-----= ", + " ***##*####****+==*++-----= ", + " *+*###*******++==*++----- ", + " =+*+***++***+++==*==----- ", + " +*****##**+-----*=====-= ", + " #******#*********======= ", + " +#*****************====== ", + " +####***** ##+*******=== ", + " ++=-- ####******+* ", + " ===--- #####***** ", + " ==--- #####* ", + " -- ##- ", + " " + ], + [ + " ", + " ######## ", + " ###%%%%%%### ", + " %%%%%%%%%%%#### ", + " %%%%%%%%%%%%%%%###** ", + " #%%%%%%%%%%%%%%%%#*#** ", + " ##%%%%%%%%%%%%######*#*- ", + " #######%%%%%%%%########***- ", + " #%#########%%%#######*#*-** ", + " *-#***########***+++-==+*=== ", + " %=#****#######*++++=---**=== ", + " ******####***++++==---:*=- ", + " ===**-*****++++++===---**-= ", + " -+*****-********:::---=*-- ", + " *%####*********++++++-*-- ", + " *#####**********+++++-*-- ", + " *####*##***+*************+ ", + " +++ ##*##*********# ", + " ==- #**###***** ", + " =+- #**#### ", + " =- #** ", + " " + ], + [ + " ", + " ##%%%%%%%# ", + " ####%%%%%*%%%%%# ", + " #%%%%%%%%%%%%%%#####* ", + " %%%%%%%%%%%%%%%#######++ ", + " %%%%%%%%%%%%%%%%########++ ", + " %%####%%%%%%%%%%%%*****####++ ", + " %%#########%%%%%%##*****+####++ ", + " +%%%##############*******#####+= ", + " *+*+*##########******====++==#-- ", + " %*+***#########****++==---*= #- ", + " %#*+**########***+++=----==##- ", + " +++=*###=****++++++++----*==##: ", + " =-=***######++****++---===+* #:: ", + " +**%%#####********++=====--##- ", + " **########********++=====:-=# ", + " **#######**##+++****#=****#* ", + " **%+++*************##== ", + " == ************ ", + " == *******+ ", + " =+ ***- ", + " " + ], + [ + " ", + " %%%%%%%%%%%# ", + " ####%%%%%%%*%%%%%%* ", + " ###%%%%%%%%%%%%%%%*####* ", + " #%%%%%%%%%%%%%%%%%%***#####= ", + " %%%%%%%%%%%%%%%######***##=##= ", + " %%%%%%##%%%%%#########++++#####= ", + " %%%%%%%################+++++#####= ", + " +**%%%############*###++++++#* ###- ", + " *****:**####*********++++---*+ ##- ", + " *+*****+*###********+++==--==+ ## ", + " **%%#**+***#*******+++===-:=* ##- ", + " %++++--####=****+++=====::==+ ##= ", + " ==--+##########*++**++==-=++* ###- ", + " ***###%%%######*****++===-::= ##= ", + " *****###*#####******++===-::== # ", + " +*#**###**###%***++++*###**##* ", + " +*##*%++++********##=# ", + " +*#++*+++******# ", + " *== *+++*** ", + " = *+= ", + " " + ], + [ + " ", + " #######%%%##* ", + " #####*%%%%%##*###### ", + " ###%%%%%%%%%%%%#####*##### ", + " ####%%%%%%%%%%%%%######**#-###= ", + " %%+#%%%%%%%%%##%%#####******##*## ", + " -%%%%%%%%%###%#########***====##=*#= ", + " *%%%%%%%%%#############***====###+**= ", + " +++***%%%%#######*********===== #*%- ", + " #%******#*+=***********++===---* ## ", + " *=*+******+=***********+===---* ## ", + " ** #%%#**+=***********===----* ### ", + " *#%++++++-##***=**++===--=---* ##+ ", + " #+++--+####*********==+===--++ ### ", + " =#-***###%%%######*++++===---- ##+ ", + " *######*###*+###**+++++===---- = ", + " **#####*###**#**##++++==##**** ", + " *#####*%%++*+++***# ", + " *####+++++++ ", + " **=*==++ ", + " += +=+ ", + " " + ], + [ + " ", + " ############# ", + " #####*%%%%%##*######+ ", + " ###%%%%%%%%%%%####*******# ", + " #**%%%%%%%%%%%%######***++*-*#* ", + " %%=%%%%%%%%%%%########**++++****** ", + " ::%%%%%%%%%########*******+++--****** ", + " **=%%%%%########*********+++--- **=** ", + " **++****%%%%%##**********=++---: *+*%= ", + " #%%********#+==****+++++++=---= *** ", + " *-+ #+%*******-+***+++++++----= *** ", + " *+ %%%%%#***-++**++++++=----+ *** ", + " ###%%%+++++--****==++==---::-+ ** ", + " %#+++==++************+-==---* *** ", + " +*=****###*%%%*******====---= ** ", + " **#######****+******=====---= - ", + " **########***++*****==+---* ", + " **########%%**** ", + " **#####** ", + " **#*+= ", + " *+ ", + " " + ], + [ + " ", + " ************* ", + " ******+%%#####+***** ", + " %***%%%%%%%%######***++*** ", + " %**=%%%%%%%%#########****=+:**+ ", + " *#%-%%%%%%%%%%%####*****++===++++ ", + " --%%%%%%%%%%%###***#***++++==:++++++ ", + " +#**==%%%#######***++++++++===--+++++ ", + " #*****#*++++####***++++++++===:- +=++ ", + " *%%%%##**+++++#*+-+++++=====-:-+ ++ ", + " **-:+==#*#+++++++-+++++=====-:= *++ ", + " =+-%====%%%%%-*++-++++++====--= *++ ", + " **-+##%%%=++====++++--==--::-= +++ ", + " ##:%%%+====++*****+++++++-:--- #++ ", + " *::+#*=****#***%%%+++++++----- +++ ", + " #::##*########****-+++++------ ", + " #:-##*#######*****=+++#+*---- ", + " :-##*#######**%%** ", + " :-##*####### ", + " :-###%%+ ", + " : #+* ", + " " + ], + [ + " ", + " ++++++++++++ ", + " %+++++=%%###*=+++++ ", + " %++%%%%%%%%%###***+==++ ", + " %++-%%%%%%%%%#####**+++=-++= ", + " #*#%%%%%%%%%######*+++++=-==== ", + " --#%%%%%%%%####***++*+====--==== ", + " -*#**#++%%%%#******+++======--===== ", + " =***##***#*++++#***++====----- -===* ", + " #%%%%%###*++++++=+=+===------ #== ", + " *#+--=--+#*=*+++==+++===------ #== ", + " +=+----=+%%%###*++++===-----= #== ", + " ###+-###%%%%-==---====----:-+ #== ", + " #-%--%%%+===+++*++++======---= #== ", + " *=---++*=******++###======--- #== ", + " *----##********#*+++-=====--- # ", + " *----###*******#*+++-===#=-:== ", + " *----###*********%%+* ", + " =---###******* ", + " =- ###*%%* ", + " %= #** ", + " " + ], + [ + " ", + " %========= ", + " %=====###**-==== ", + " %==-%%%%####****=-== ", + " %%==%%%%%#####****++=-=- ", + " ###*%%%%%%%###*****+===:-- ", + " %+=#%%%%%%%%%***+++==+---:-- ", + " ==**%**#-===###**++++==-------- ", + " +++****#****#++===***==---::-=-- ", + " *###%%%%%%###++=====-==:::--=#-- ", + " *+###=::+=++-**========-::---#-- ", + " %+++--==++++%%#***+===-:::-=#-- ", + " %#-++=-**###%%%---::*------=%#- ", + " %*=:%=--%%%%=--++*+===------=%-- ", + " #=-----*=*=++++++==###----==%-- ", + " +=-----***+++++++*+===----=+= ", + " +==----***+*+++++*+===--#-=- ", + " +==---=*****+++++++### ", + " +== *****+++%+= ", + " %+ ****%## ", + " + #*+ ", + " " + ], + [ + " ", + " %%------ ", + " %----##*+--- ", + " %%%-%%%%%####*+: ", + " #%%%%-%%%%%***##+++= ", + " #%%###+%%%%%##**++++=- ", + " %%%%++%%#####%##+++==--: ", + " %=*#%*%%+**===%***+===---- ", + " +**###+*++**++++*+---++----- ", + " +**%%%#%%%##%****=----:%---= ", + " =#=--###---*++##*+----:%---= ", + " #==:+++:==+++##%##++------= ", + " %%#+-#+*--++***%%##-:-=:-== ", + " %%#=-:%%--+##%%---**=--:--- ", + " +*=-::--=*===+=====-**---- ", + " **---:--==**=+=====+-----+ ", + " **---:--==**=++====+=---##+ ", + " **-- **=+++=====#* ", + " %%* ***++++=== ", + " +#*+ ***+## ", + " *+ ** ", + " " + ], + [ + " ", + " %--- ", + " %%-=#*- ", + " %%%%%-#####**+ ", + " #%%%%%%%-%%%%%****= ", + " %%%%%%###=%%%##****==: ", + " #%%%%%##+*#######**==--: ", + " %%--*=+#=#+=*----#++=--- ", + " *+***##*#%++=*++++*==--=-= ", + " %=###%%%%%#######***-=---%= ", + " **#---:####---**%%-==---#= ", + " =%#----++++:****%%%**==:-= ", + " %%%#*=--#-#:+*++++%##=-==+ ", + " -%%%---%%*++***%#==#---= ", + " +***----=+=++---------+= ", + " *****---==+*+*++-----==-= ", + " *****---===*+ ++-=---=---# ", + " *****= ++-=-----** ", + " #****= ++-=---%- ", + " *#**= ++==** ", + " +*= *+ ", + " " + ], + [ + " ", + " %# ", + " %%%%%* ", + " %%%%%%%=###** ", + " %%%%%%%%%%=####+++ ", + " ##%%%%%%%#######***+-- ", + " *#%%%%%%%###+##******+-= ", + " ##%%%=--**####=-+----==- ", + " +*++***###**#==++===++-= ", + " ##++****%%%%%#*#****+++:==== ", + " %+***:-+--:#**#==*#**--+=# ", + " +##%%--+++++=++#####*%++-= ", + " +%%%%#%==+***#**#===#**= ", + " ===%%%%+++%%****++#-+# ", + " +****+#+++******-=*---= ", + " +#****##+++******==-:::- ", + " +#****## -** ==-:::-# ", + " ++#****## ==-:::-% ", + " *#****## ==-::%: ", + " *#*## ==-%+ ", + " ** += ", + " " + ], + [ + " ", + " ##*%# ", + " *%%%**%# ", + " %%%%%%%%*%#*** ", + " %%%%%%%%%%#%##***== ", + " #%%%%%%%%%%%%******++= ", + " ##%%%%%%%%%%%**==**+**+= ", + " *###%%%%%::++*****-=---+- ", + " +##%**++******+++#--=---=- ", + " +#********%%%%%%**++++===-% ", + " +***##**=##---=****+*#*==- ", + " ++#%%%%=######=-=+###**== ", + " :*++%%%%####***=-*###--*+ ", + " +**=-==+#%%%***##**##=*= ", + " ****####=*+*****#****=*= ", + " ***######*******#***#:-=- ", + " #***######*** ###**#:-=- ", + " ##**#####*** #:-== ", + " #%#####*** #:--%= ", + " **##** ::-%= ", + " *** #= ", + " " + ], + [ + " ", + " ######## ", + " ##%%%%%##### ", + " #%%%%%%%%%%###* ", + " %%%%%%%%%%####*%**+- ", + " #%%%%%%%%%%%%#%%%+=++= ", + " ###%%%%%%%%%%%%%*+++-++= ", + " #####%%%%%%%%%--=-++*+---=+ ", + " **##%%****=++++***=-*:----: ", + " +***+*******+++%%%##*++===:: ", + " =******####*###----=++++*+=- ", + " +**++#%%%%%####*###=--**+- ", + " +=***=+++%%%%###*##+#+**--* ", + " *****#=-++++*#%%##:****%-+ ", + " ***#########=*+********+- ", + " ***##**######*********+#- ", + " #**+##**######*********+#- ", + " %%*#**######*** +#- ", + " #%*#####*** #-% ", + " **##*** #-# ", + " +++ #- ", + " " + ], + [ + " ", + " %%%%%%#### ", + " %%%%%%%%%%%####* ", + " ####%%%%%%%%%%%%###** ", + " ####%%%%%%%%%%%%%#####** ", + " ##%%#%%%%%%%%%%%######%%== ", + " ######%##%%%%%%%##%%%%%%+=+=- ", + " ##=########%%%%%%%%%%:=--==*=-+ ", + " -#=#######%%%%***++-====++--+=-= ", + " ##-=******#*******++++###**+==-- ", + " #- **#*******####++***----====+ ", + " #-+**##***+##%%###**######---# ", + " ##+=+++**##+++++%%%###***#===*= ", + " ##+ +*****####=--=+=+*###*-+-+%= ", + " #==++###########***-===+***++* ", + " =+++#######**#####**+++****+* ", + " +*####*###**####***+++****+* ", + " +%###**###****+++****+* ", + " ##*#*****+++ +* ", + " =++**+++ *# ", + " =+=* #= ", + " " + ], + [ + " ", + " %%%%%%%%%### ", + " %%%%%%%%%%%%%#####+ ", + " ####%%%%%%%%%%#########+ ", + " ###-#%%%%%%%%%%%%%####***##+ ", + " ##=##%%%%%%%%%%%%%%###*****%%- ", + " ##=####%%%%%###%%%%%%#**##%%#--+ ", + " ##=#*###%%######%####%%%####+::%*+ ", + " %##* +##########%##%***++:---==:-%+ ", + " ### ++**********=**+++++===***++-= ", + " ## ++***##******++####++##====*-- ", + " ##- +****##***==#%%%%**####****=: ", + " ##= +*+**+-####=++++++##***++*-=* ", + " ###- ***#######**#===-=====***-=*% ", + " ##- ****###########*****-+=-=**** ", + " = *****#########++*******===**** ", + " =*############++*****+*===**** ", + " +%%###++*****+*==***** ", + " #******++==***** ", + " =++++== *** ", + " ==- * ", + " " + ], + [ + " ", + " %%%%%%%%%#### ", + " %%%%%%%%%%%%%%###### ", + " ####%%%%%%%%%%%%%%####*##= ", + " ###:#%%%%%%%%%%%%%%%%%***++*##= ", + " ##-###%%%%%%%%%%%######***+++*%%= ", + " ########%%%%%%%%%###%%%###*++#####-+ ", + " ######*#%%%%%%%#######%####%##****--* ", + " #### **%%%%########%%#%%***+=-::-*-% ", + " ## =*+*###*#******+****++++=::++==- ", + " ## *+**##**#********++****=-*++++: ", + " ##= =+***#**###****==*%%%+++*## *+ ", + " ### +**++***+-*****==+==+#**++==*: ", + " ##: -=***************+=--==--+**-% ", + " ### ******######*******++++++-:-*+ ", + " - #*****#######****=+++++++---*+ ", + " ++#*#########****=+++++++--**+ ", + " ++%***+++++++++--** ", + " *+++++++--** ", + " ==-+--** ", + " ==- *- ", + " " + ] + ] + }, + { + "width": 40, + "height": 10, + "frames": [ + [ + " ", + " ###*###* ", + " #-%%%%%%##+=* ", + " *##%%%%##***#+* ", + " * +******++===- ", + " * ****=**##*=* ", + " * ******##+==: ", + " +***==-: ", + " :: ", + " " + ], + [ + " ", + " %**+#*** ", + " *=*%%%%%#**=+ ", + " +**#%####**+*++ ", + " + ******++*=-- ", + " + ******-++**+ ", + " + *#*****##=-- ", + " +++++--- ", + " #=- ", + " " + ], + [ + " ", + " **+#+++ ", + " +*%%%%###+- ", + " =+%#%####++=# ", + " = +****++++*-- ", + " = +****++-=-+: ", + " % ###***++*+-+ ", + " +==+++==: ", + " **: ", + " " + ], + [ + " ", + " ===*== ", + " =+%%%##*= ", + " -=%##****=+= ", + " *++*+**+===+= ", + " *++***++=---= ", + " -*###+++==+= ", + " +-=++*+-:= ", + " ** + ", + " " + ], + [ + " ", + " %--*: ", + " --%%#*+- ", + " %-###*++-= ", + " +-*++*+--:+ ", + " +-*++*+=-:= ", + " +####===:= ", + " *--==+ *- ", + " +* * ", + " " + ], + [ + " ", + " %-- ", + " %%+#**+ ", + " #%%#**=== ", + " +**+++==:-- ", + " +**=+++=--+ ", + " ##=#**--- ", + " +=--= **= ", + " -+ ** ", + " " + ], + [ + " ", + " *% ", + " %%%***+= ", + " #%%%%%*+-: ", + " *##*++=--- ", + " ***+++==:= ", + " +*#**#++-- ", + " +-= #** ", + " =- ##* ", + " " + ], + [ + " ", + " *## ", + " %%%%##*- ", + " #%%####+= ", + " ***##*=*--= ", + " =+***+=*--= ", + " ******==- ", + " ++ #**** ", + " - ### ", + " " + ], + [ + " ", + " #### ", + " %%%%%%## ", + " ##%%%%###* ", + " #*###*++-*= ", + " **#**+=--* ", + " %#****++#- ", + " #+ *#***= ", + " - *# ", + " " + ], + [ + " ", + " ##%%%% ", + " %%%%%%### ", + " %###%%%#**## ", + " *-####**+=-+- ", + " +=#=#**++-*#: ", + " *%##***+==-# ", + " *#+*****#* ", + " = ** ", + " " + ], + [ + " ", + " ##%%%%% ", + " %%%%%%%#*## ", + " %%%######++## ", + " ***##***++-* # ", + " +%+#***++== # ", + " +*%%##**+=-: * ", + " +###+***## ", + " * *+* ", + " " + ], + [ + " ", + " #######* ", + " ##%%%%%##*### ", + " %%%#####**=### ", + " #***+*****==- # ", + " %++#*=**+=- #+ ", + " *##%%##*+=-- # ", + " *####**** ", + " *== ", + " " + ], + [ + " ", + " ######## ", + " *%%%%%###*+-* ", + " +%%####***++-** ", + " #%******+++-= * ", + " ##%+%-*=++=- * ", + " *#*#%%***=-- * ", + " *###*%** ", + " #+ ", + " " + ], + [ + " ", + " ******** ", + " *=%%%###**=:+ ", + " **%%###*+*+=-++ ", + " %%*++--++==- + ", + " *#%=#=+-==-= + ", + " +#***%%+++-- + ", + " :####*** ", + " -#% ", + " " + ], + [ + " ", + " %++++++ ", + " +%%%%##*+-+ ", + " =*%%%**+===-= ", + " #%%#++=%==-- = ", + " *-#%%=#=---= = ", + " *-#***+#-==- # ", + " -#***#+%# ", + " ##* ", + " " + ], + [ + " ", + " %===== ", + " %%%##**+- ", + " *%%=%#+++=-- ", + " ##%%%+==-::-- ", + " *+=*#%%-+--=- ", + " +-**+++*#--% ", + " +-#**+++== ", + " + *# ", + " " + ], + [ + " ", + " %---- ", + " #%-%%**+ ", + " %%%*=%*==- ", + " %%#%#%=---= ", + " #-*=**#-+== ", + " *--=*===+- ", + " *- *+==-: ", + " * *# ", + " " + ], + [ + " ", + " %-- ", + " %%%%%** ", + " %+%#=##+- ", + " =%%%###*=-= ", + " %##+**+*#-+ ", + " *=-+++--+ ", + " #*- +=--- ", + " %* +* ", + " " + ], + [ + " ", + " == ", + " %%%%%#*+ ", + " #%%*##-**- ", + " +*%%#***+= ", + " +%%=+*#=#+ ", + " %*#=+**=:= ", + " *** =:- ", + " +*- =+ ", + " " + ], + [ + " ", + " **# ", + " #%%%%%** ", + " ##%%-**-+ ", + " =***%%#*+++ ", + " +%%###=-#-= ", + " %##*****: ", + " **##* == ", + " **- - ", + " " + ], + [ + " ", + " #%## ", + " %%%%#%*+ ", + " ##%%%%=*%= ", + " +****%%#+== ", + " :%+%###=*: ", + " *%###****- ", + " *#*##* *- ", + " ** - ", + " " + ], + [ + " ", + " %%%##* ", + " -%%%%%### ", + " +###%%%%+=-= ", + " #*#*+**+##*=- ", + " #+*:#%+%#**-= ", + " =+#%%#*#+**= ", + " ##*##*+-*+ ", + " ++ + ", + " " + ], + [ + " ", + " %%%%##+ ", + " ##%%%%%#**# ", + " -##%####%##-* ", + " # +*****++=*+- ", + " # **-#%%#**=- ", + " - *###%#***=** ", + " ###+**+=** ", + " =+= * ", + " " + ], + [ + " ", + " %%%%%### ", + " #:%%%%%#%*+*# ", + " ##*%%%######*- ", + " # **##**+*+=+=- ", + " #- *#*+*-%=+++ ", + " # **###%%+++-- ", + " ****+++-* ", + " --# ", + " " + ] + ] + }, + { + "width": 28, + "height": 5, + "frames": [ + [ + " ", + " -%%# ", + " **+- ", + " =- ", + " " + ], + [ + " ", + " *%%# ", + " +**+-+ ", + " +*- ", + " " + ], + [ + " ", + " +%#* ", + " =+*+-= ", + " ++- ", + " " + ], + [ + " ", + " %%%* ", + " +++= ", + " -+ - ", + " " + ], + [ + " ", + " -#+ ", + " *++: ", + " =* ", + " " + ], + [ + " ", + " %*+ ", + " *++- ", + " +- * ", + " " + ], + [ + " ", + " %* ", + " *+=- ", + " - + ", + " " + ], + [ + " ", + " %%# ", + " #*=- ", + " : *# ", + " " + ], + [ + " ", + " %%# ", + " *#== ", + " ** ", + " " + ], + [ + " ", + " %%#+ ", + " ##+= ", + " ***# ", + " " + ], + [ + " ", + " %%%# ", + " =+*+=- ", + " *** ", + " " + ], + [ + " ", + " %%## ", + " +***-# ", + " #+* ", + " " + ], + [ + " ", + " %%#* ", + " *++- ", + " ## ", + " " + ], + [ + " ", + " %##* ", + " =*#+:* ", + " ##* ", + " " + ], + [ + " ", + " %%*+ ", + " +=+=-= ", + " -** ", + " " + ], + [ + " ", + " =#*= ", + " -=%- ", + " + += ", + " " + ], + [ + " ", + " %%* ", + " ++-- ", + " *= ", + " " + ], + [ + " ", + " #%* ", + " -+%- ", + " * =* ", + " " + ], + [ + " ", + " %# ", + " *+#+ ", + " * : ", + " " + ], + [ + " ", + " %%= ", + " +==* ", + " ## : ", + " " + ], + [ + " ", + " %%+ ", + " *-#- ", + " *# ", + " " + ], + [ + " ", + " #%%# ", + " *+## ", + " %#+* ", + " " + ], + [ + " ", + " #%%* ", + " #**-#= ", + " ++* ", + " " + ], + [ + " ", + " #%%# ", + " =*#*+* ", + " %+- ", + " " + ] + ] + } + ] +} diff --git a/assets/runa-cover.png b/assets/runa-cover.png new file mode 100644 index 0000000..aa92d47 Binary files /dev/null and b/assets/runa-cover.png differ diff --git a/docs/ascii-turntable.md b/docs/ascii-turntable.md new file mode 100644 index 0000000..f42b32f --- /dev/null +++ b/docs/ascii-turntable.md @@ -0,0 +1,52 @@ +# Modelos Tripo en la pantalla de inicio + +El juego reproduce 24 vistas de texto precalculadas. No abre GLB, no usa un +motor 3D y no llama a Tripo durante la partida. El reloj existente cambia el +cuadro cada 125 ms: una vuelta tarda tres segundos. + +Para convertir un modelo una vez, desde Ubuntu con Python 3 y numpy: + +```bash +python3 scripts/glb-to-ascii.py assets/tripo/heroe.glb --output assets/ascii/heroe.json +python3 scripts/glb-to-ascii.py assets/tripo/coloso.glb --output assets/ascii/coloso.json +python3 scripts/glb-to-ascii.py assets/tripo/yelmo.glb --output assets/ascii/yelmo.json +python3 scripts/glb-to-ascii.py assets/tripo/coloso.glb --output assets/ascii/coloso-field.json --sizes 43x13 +python3 scripts/glb-to-ascii.py assets/tripo/paisaje.glb --hero assets/tripo/heroe.glb --colossus assets/tripo/coloso.glb --output assets/ascii/paisaje.json +``` + +El script lee la escena GLB, aplica las transformaciones de sus nodos y +rasteriza sus triangulos con un buffer de profundidad desde 24 angulos. +Usa luz sobre la geometria para elegir caracteres ASCII; no muestrea las +texturas. Produce tres tamanos para adaptar la portada a la terminal. +Los GLB comprimidos con Draco requieren descompresion previa. + +Los cuadros JSON se incluyen en el juego y pueden versionarse; los GLB +permanecen fuera de git. El menu muestra una composicion del reino, el heroe y +el Coloso. El paisaje generado no incluyo los combatientes solicitados, por lo +que el conversor los incorpora reutilizando sus GLB, sin otro pedido a Tripo. +El mapa del jefe reutiliza el Coloso sobre el lienzo original 43x13; sus avisos +y poderes siguen siendo los del juego. La creacion muestra el heroe. Al seleccionar el +yelmo de hierro en la armeria o el inventario aparece su vista ASCII, si hay +espacio suficiente. Se compra desde nivel 3 por 75 de oro y se equipa con +Enter en la mochila (I); conserva sus valores de defensa +2 y velocidad -0.04. +Para usar otro modelo, convertirlo y cambiar los imports. En terminales sin +espacio suficiente se mantiene el logo estatico y los controles existentes. + +## Pedidos reales usados en esta actualizacion + +Los cuatro GLB se generaron con `P1-20260311`, 3000 caras, textura y PBR. +Cada respuesta reporto 40 creditos consumidos. Los prompts fueron: + +- Heroe: `a low-poly medieval hero with a round shield and a short sword, single character, game prop` +- Yelmo: `a low-poly iron helmet with a single rune on the brow, game prop` +- Coloso: `a massive ancient stone colossus, full body standing upright, weathered volcanic rock armor with glowing amber rune carvings, broad shoulders, two arms and two legs, single isolated character, low-poly medieval fantasy game boss` +- Paisaje: `a single cohesive low-poly medieval fantasy diorama on a round terrain base, a walled kingdom with castle towers in the background, outside the walls a small hero with sword and round shield facing a giant stone colossus in battle, volcanic rocky ground, amber rune accents, clear readable silhouettes, full scene isolated, game title screen diorama` + +El conversor conserva la geometria producida por esos pedidos. La textura +PBR se puede revisar en el visor, pero no interviene en la conversion a ASCII. + +La ilustracion `assets/runa-cover.png` se genero con la herramienta integrada +de imagenes como portada grafica independiente. Prompt: portada horizontal +de RUNA, heroe con espada y escudo frente a un coloso de piedra en ruinas +volcanicas, torre central y castillos, runas ambar, titulo RUNA y espacio +oscuro inferior para el menu. El menu de terminal usa los cuadros ASCII. diff --git a/docs/demo-walkthrough.md b/docs/demo-walkthrough.md new file mode 100644 index 0000000..35a7c65 --- /dev/null +++ b/docs/demo-walkthrough.md @@ -0,0 +1,27 @@ +# Demo grabada de RUNA y Tripo + +El video muestra 68 segundos de recorrido por el render real de RUNA: +menu con paisaje, creacion de personaje, ciudad, NOX, pradera, compra del +yelmo, inventario y arena del Coloso. + +Es una grabacion reproducible del render ANSI presentado en un navegador, +no una grabacion de una ventana nativa de terminal. El script ejecuta Runa, +sus entradas de teclado y su reloj a 15 cuadros por segundo. El navegador +solo reproduce esos cuadros para capturarlos; no reemplaza el juego. + +La partida de muestra vive en memoria y no lee ni escribe ranuras del jugador. +Los cambios entre zonas y el estado nivel 3 / 100 oro se preparan como fixtures +de demostracion: no representan una subida de nivel obtenida durante el video. +La compra del yelmo se ejecuta con la entrada real Enter y consume oro. + +```bash +npx bare scripts/demo-walkthrough.js output/playwright/demo +python3 -m http.server 4176 --bind 127.0.0.1 --directory output/playwright/demo +``` + +Abrir `http://127.0.0.1:4176/`, esperar a `window.demo.ready` y ejecutar +`window.startDemo()` para reproducir. La secuencia termina cuando +`window.demo.done` es true. `chapters.json` registra los tiempos y titulos. +La captura se realiza con Playwright a 1280x900 y 15 FPS, y se convierte a +MP4 H.264 con FFmpeg para compartirla. No se utilizan assets de muestra ajenos +ni se genera un trailer con escenas inventadas. diff --git a/docs/demo/runa-tripo-demo.mp4 b/docs/demo/runa-tripo-demo.mp4 new file mode 100644 index 0000000..60a9cb0 Binary files /dev/null and b/docs/demo/runa-tripo-demo.mp4 differ diff --git a/docs/lore.md b/docs/lore.md new file mode 100644 index 0000000..64ab565 --- /dev/null +++ b/docs/lore.md @@ -0,0 +1,34 @@ +# Lore de RUNA + +Notas del equipo, 8 de octubre de 2026. Marcan la dirección del mundo, no un +diseño cerrado: lo que figura en "A definir" todavía no se decidió. + +## Para qué existe RUNA + +RUNA existe para romper la cuarta pared. + +La pantalla de título ya lo dice: "no controlas a tu personaje. escribis las +reglas que sigue." El jugador no está adentro del mundo moviendo al héroe: está +afuera, escribiendo las reglas que el héroe obedece. + +## La torre del centro + +En el centro debe nacer una torre para ir a otros mapas. + +A definir: + +- El centro de qué: la plaza de RUNA, la pradera entre RUNA y NOX, u otro lugar. +- Si la torre está desde el inicio o nace durante el juego, y qué la hace nacer. +- A qué mapas lleva y cómo se elige el destino. +- Qué relación tiene con el portal que ya existe en la pradera + ([runa-design.md](runa-design.md)). + +El arte y el mapa de la torre los hace Codex. Los mapas y sus salidas se +registran en `lib/map.js`, como la salida `Q` del Coliseo. + +## Ideas en discusión + +- La ventana 3D al lado de la terminal (el arma equipada en 3D, el jefe mundial, + los efectos de combate y el clima) rompe la cuarta pared de forma literal: el + juego se sale de la consola. Está en estudio para el Demo Day; la base actual + está en [tripo-integration.md](tripo-integration.md). diff --git a/docs/screens/tripo-coloso-mapa.png b/docs/screens/tripo-coloso-mapa.png new file mode 100644 index 0000000..895d194 Binary files /dev/null and b/docs/screens/tripo-coloso-mapa.png differ diff --git a/docs/screens/tripo-coloso.png b/docs/screens/tripo-coloso.png new file mode 100644 index 0000000..61e96ef Binary files /dev/null and b/docs/screens/tripo-coloso.png differ diff --git a/docs/screens/tripo-heroe.png b/docs/screens/tripo-heroe.png new file mode 100644 index 0000000..7d25f82 Binary files /dev/null and b/docs/screens/tripo-heroe.png differ diff --git a/docs/screens/tripo-modelos.png b/docs/screens/tripo-modelos.png new file mode 100644 index 0000000..798faa8 Binary files /dev/null and b/docs/screens/tripo-modelos.png differ diff --git a/docs/screens/tripo-paisaje.png b/docs/screens/tripo-paisaje.png new file mode 100644 index 0000000..94c3eaa Binary files /dev/null and b/docs/screens/tripo-paisaje.png differ diff --git a/docs/screens/tripo-personaje.png b/docs/screens/tripo-personaje.png new file mode 100644 index 0000000..dfb0e49 Binary files /dev/null and b/docs/screens/tripo-personaje.png differ diff --git a/docs/screens/tripo-yelmo-inventario.png b/docs/screens/tripo-yelmo-inventario.png new file mode 100644 index 0000000..e822be4 Binary files /dev/null and b/docs/screens/tripo-yelmo-inventario.png differ diff --git a/docs/screens/tripo-yelmo-tienda.png b/docs/screens/tripo-yelmo-tienda.png new file mode 100644 index 0000000..6f61899 Binary files /dev/null and b/docs/screens/tripo-yelmo-tienda.png differ diff --git a/docs/tripo-integration.md b/docs/tripo-integration.md new file mode 100644 index 0000000..0548423 --- /dev/null +++ b/docs/tripo-integration.md @@ -0,0 +1,625 @@ +# Tripo en Runa, para el Demo Day + +> Actualizacion implementada: los modelos reales de heroe, yelmo y Coloso ya +> fueron generados y convertidos a 24 cuadros ASCII. El menu muestra +> el paisaje del reino compuesto con heroe y Coloso; la creacion muestra al heroe y tienda/inventario muestran +> el yelmo. El juego carga esos cuadros de texto sin usar la API ni abrir GLB. +> Ver [ascii-turntable.md](ascii-turntable.md) y el [README](../README.md). +> El plan de demo con dos ventanas que sigue abajo fue la propuesta inicial; +> las secciones de API y CLI describen el generador y el visor opcional. + +Documento de integracion. Fecha de lectura de la doc: 8 de octubre de 2026. +El Demo Day del Tripothon S1 es el 11 de octubre de 2026, en Sao Paulo. + +Runa 0.2.0 es un RPG de terminal. `index.js` arranca `bare-tui` y el juego vive +en `lib/`. No hay servidor web ni visor 3D. Tripo no entra al loop del juego: +genera un GLB al lado, y el ASCII sigue igual. + +La API que vas a usar es la v3, base `https://openapi.tripo3d.ai/v3`. La v2 +sigue publicada en `https://platform.tripo3d.ai/docs/` y el banner de ese sitio +dice que desde el 1 de octubre de 2026 (00:00 UTC+8) no tiene soporte, y que el +1 de noviembre de 2026 (00:00 UTC+8) los endpoints v2 dejan de aceptar pedidos. +El 11 de octubre la v2 todavia responderia, pero el demo se arma contra la v3. + +## 1. Que se ve el 11 + +Una sola escena, en dos ventanas de la misma notebook: + +1. A la izquierda, `npm start`. Caminás la ciudad o entrás a las ruinas del + Coloso. El juego no llama a Tripo. +2. A la derecha, el visor local (`npm run tripo:viewer`) con tres GLB ya + generados: heroe con espada y escudo, cofre de hierro con runas, coloso de + piedra. Los generás antes, con texto, modelo `P1-20260311`, textura standard. +3. En vivo, un cuarto pedido de texto: un yelmo de hierro con una runa. El + script hace el POST, consulta la tarea cada 2 segundos y, cuando `status` es + `success`, baja `output.model_url` a `assets/tripo/`. El visor lo muestra solo + a los pocos segundos, sin recargar. + +La ficha de P1 publica unos 60 segundos con textura y unos 10 sin textura. El +quick start habla de 10 a 120 segundos en general, y el ciclo de vida recomienda +cortar el poll a los 5 minutos. Si a los 2 minutos la tarea sigue en `queued` o +`running`, cortás el en vivo y dejás los tres GLB que ya estan en pantalla. El +script lo hace solo con `--timeout 120`, aunque el wifi se cuelgue. Si la tarea +termina despues del corte, Tripo la cobra igual: el script deja impreso el +comando para bajarla con `--task` sin pagar otra. + +Ese es todo el demo. No hay rig, animacion, ni modelo metido en la grilla ASCII. + +Prompts de respaldo, en ingles, como los ejemplos de la doc. El soporte de +prompts en espanol en la v3 queda sin verificar (la v2 sí dice que acepta varios +idiomas). + +```text +a low-poly medieval hero with a round shield and a short sword, single character, game prop +a small iron treasure chest with rune carvings, low-poly game prop +a stone colossus standing in volcanic ruins, low-poly game boss +a low-poly iron helmet with a single rune on the brow, game prop +``` + +Cada prompt entra en el tope de 1024 caracteres. + +## 2. Auth + +1. Entrás a la consola: https://platform.tripo3d.ai +2. Abrís API Keys y creás una key. Se muestra una sola vez. +3. La guardás en el entorno, con el nombre que publica la doc v3: + +```powershell +$env:TRIPO_API_KEY = "your_api_key_here" +``` + +```bash +export TRIPO_API_KEY="your_api_key_here" +``` + +Todas las llamadas llevan: + +```text +Authorization: Bearer {api_key} +``` + +La key no va en el HTML, ni en `save.json`, ni en el repo. `.env` y `.env.*` ya +estan en `.gitignore`. El proceso de Bare del juego no tiene por qué ver esa +variable: el script de Tripo es otro proceso, con Node. + +La guia de migracion dice que la misma key sirve para v2 y v3. El prefijo real +queda sin verificar: la v2 dice que empieza con `tsk_`, el quick start v3 muestra +el placeholder `sk-...`, y la pagina de auth muestra `your_api_key_here`. El +script no valida el prefijo: acepta lo que te copie la consola. + +`tcli_` es un Client ID de la v2, no una key. Si una llamada responde 401, estás +usando el id equivocado o la key no viaja en el header. + +Probá la key con el saldo antes de gastar: + +```powershell +npm.cmd run tripo -- --balance +``` + +Es la misma llamada que: + +```bash +curl -X GET https://openapi.tripo3d.ai/v3/account/balance \ + -H "Authorization: Bearer ${TRIPO_API_KEY}" +``` + +```json +{ + "code": 0, + "data": { + "balance": 10000.0, + "frozen": 200.0 + } +} +``` + +`balance` es el saldo disponible. `frozen` es lo que ya quedo apartado en tareas +en curso. Los numeros del ejemplo son de la doc, no tu saldo. + +Una key invalida, probado el 8 de octubre contra ese endpoint, devuelve HTTP 401 +con este cuerpo: + +```json +{ + "code": 2, + "status": "error", + "message": "Invalid API key", + "suggestion": "Check if your credentials is valid" +} +``` + +La doc publica `1000` o `1001` para ese caso. El script no depende del numero: +muestra el cuerpo completo y, con 401, avisa que revises la key. + +## 3. Endpoints + +No hay un endpoint aparte para "bajar el GLB". Creas una tarea, consultas hasta +`success`, y hacés GET a la URL que viene en `output.model_url`. El quick start +dice que esa URL es el archivo GLB, que vence a los 5 minutos, y el ejemplo +termina en `.glb`. El script la baja en el mismo proceso, apenas la tarea +termina. + +Respuesta comun de alta, texto o imagen: + +```json +{ + "code": 0, + "data": { + "task_id": "task_abc123" + } +} +``` + +`code` distinto de 0 es error. El cuerpo de error publicado es: + +```json +{ + "code": 2010, + "message": "Insufficient credits", + "suggestion": "Please top up your account at https://platform.tripo3d.ai", + "request_id": "req_abc123" +} +``` + +Sin creditos: HTTP 403, `code` 2010. Key invalida: HTTP 401; la doc dice `1000` +o `1001`, la API respondio `2` (seccion 2). + +### Texto a 3D + +`POST https://openapi.tripo3d.ai/v3/generation/text-to-model` + +Para el demo usá la serie P. Es el modelo que la ficha vende para assets de +juego, y el ejemplo oficial ya trae `face_limit`. + +```json +{ + "prompt": "a low-poly iron helmet with a single rune on the brow, game prop", + "model": "P1-20260311", + "face_limit": 3000, + "texture": true, + "pbr": true +} +``` + +| Campo | En este demo | +| ------------ | ----------------------------------------------- | +| `prompt` | Obligatorio. Maximo 1024 caracteres. | +| `model` | Obligatorio. `P1-20260311`. | +| `face_limit` | 3000. Dentro del rango que publica el endpoint. | +| `texture` | `true`. El default ya es `true`. | +| `pbr` | `true`. Si es `true`, `texture` queda forzado. | + +`texture_quality` no se manda: el default documentado es `standard`. + +P1 no soporta `quad`, `smart_low_poly`, `generate_parts` ni `geometry_quality`. +No los mandes. En la serie H, `quad: true` fuerza la salida a FBX. + +La serie P tambien documenta `negative_prompt`, hasta 255 caracteres. No hace +falta mandarlo. La serie H usa el mismo path y pide `model` en `v3.1-20260211`, +`v3.0-20250812` o `v2.5-20250123`. No hace falta para el 11. + +El rango de caras de P1 no coincide entre paginas: el endpoint dice 50 a 20.000, +el encabezado de la ficha dice 48 a 20.000, y el texto de la ficha tambien dice +50 a 20.000. 3000 entra en los dos. Por debajo de 150 (simple) o 250 (complejo) +la propia doc avisa que la calidad baja. + +### Imagen a 3D + +Primero subís el archivo, si no tenés una URL pública. + +`POST https://openapi.tripo3d.ai/v3/files` + +`multipart/form-data`, campo `file`. En esta llamada no armes el header +`Content-Type` a mano: el cliente tiene que poner el boundary. + +```bash +curl -s \ + -H "Authorization: Bearer ${TRIPO_API_KEY}" \ + -F "file=@referencia.png" \ + https://openapi.tripo3d.ai/v3/files +``` + +```json +{ + "code": 0, + "data": { + "file_token": "file_abc123" + } +} +``` + +La pagina de upload acepta imagenes JPEG y PNG, maximo 20 MB. Modelos 3D en ese +mismo endpoint: GLB, GLTF, FBX, OBJ, STL, maximo 150 MB. Por encima de 60 MB +pide el flujo de archivos grandes. Una referencia para este demo no llega ahi. + +Despues: + +`POST https://openapi.tripo3d.ai/v3/generation/image-to-model` + +```json +{ + "input": "file_abc123", + "model": "P1-20260311", + "face_limit": 5000, + "texture": true, + "pbr": true +} +``` + +`input` es un solo valor. La doc dice que elijas uno: + +- el `file_token` que devolvio `POST /v3/files` +- una URL publica directa, por ejemplo `https://example.com/photo.png` +- un `task_id` de una tarea previa `text_to_image` o `image_to_image` + +Formatos de esa imagen: PNG, JPEG y WebP. Maximo 20 MB. Resolucion sugerida, al +menos 256 por 256, sujeto visible, poca oclusion, fondo limpio. + +WebP en `POST /v3/files` queda sin verificar: el endpoint de imagen lo acepta +como `input`, y la pagina de upload solo lista JPEG y PNG. El script solo acepta +PNG o JPEG. + +El quick start de "Game-Ready Character" manda `file_token` como campo raiz del +JSON. La referencia de image-to-model manda ese token dentro de `input`. El +script sigue la referencia (`input`). El flujo de rig de esa pagina no entra en +el demo. + +Una captura de la grilla ASCII no cumple lo que pide la doc (sujeto visible, +fondo limpio). Si probás imagen a 3D, usá un dibujo o una foto del objeto. + +El ejemplo de tarea terminada en las paginas de imagen repite +`"type": "text_to_model"`. El script no mira `type`. El `type` real de una tarea +de imagen queda sin verificar. + +### Consulta de la tarea + +`GET https://openapi.tripo3d.ai/v3/tasks/{task_id}` + +El id va en el path y, segun la referencia, empieza con `task_`. + +```bash +curl -s \ + -H "Authorization: Bearer ${TRIPO_API_KEY}" \ + https://openapi.tripo3d.ai/v3/tasks/task_abc123 +``` + +Cuando termina: + +```json +{ + "code": 0, + "data": { + "task_id": "task_abc123", + "type": "text_to_model", + "status": "success", + "progress": 100, + "output": { + "model_url": "https://cdn.tripo3d.ai/output/model_pbr.glb", + "rendered_image_url": "https://cdn.tripo3d.ai/output/preview.png" + }, + "credits_consumed": 100.0, + "created_at": "2026-04-28T12:00:00Z", + "completed_at": "2026-04-28T12:01:30Z" + } +} +``` + +`credits_consumed: 100.00` es un ejemplo de payload, no la tarifa. La tarifa esta +en la seccion de limites. + +`output` solo viene con `status` `success`. Ahi estan `model_url` y +`rendered_image_url`. + +Estados en la pagina de consulta: `queued`, `running`, `success`, `failed`, +`cancelled`. El ciclo de vida agrega `banned` y `expired`. El script trata los +siete como finales salvo `queued` y `running`. En `failed` pueden venir +`error_code` y `error_message`, y el script los muestra. + +`progress` va de 0 a 100. En `queued` la tabla del ciclo de vida lo pone en 0. +El script consulta cada 2 segundos. El quick start pide no pasar de 1 request +por segundo. El tope numerico de QPS de generacion queda sin verificar: la +pagina de rate limit dice que depende del endpoint y no publica el numero. + +### Descarga del GLB + +No hay path de descarga. Con `status` `success`, el GLB se baja de +`output.model_url`, sin la key en el header. El quick start dice que esa URL +vence a los 5 minutos; la FAQ de la v2 dice 60 segundos y un 403 si se vence. +Si el GET falla, el script vuelve a consultar la tarea y usa la URL nueva. Si +esa segunda URL pide `Authorization`, queda sin verificar: la doc no lo dice. + +El tamano del GLB de salida queda sin verificar. No hay un tope publicado para +el archivo generado. El tope de 150 MB es para subir un modelo, no para bajarlo. + +## 4. Donde esta el codigo + +El juego no se toca. `lib/game.js`, los mapas y el render ASCII quedan como +estan. Todo vive en `scripts/`, con Node 18 o mas nuevo (`fetch`, `FormData` y +`Blob`) y sin dependencias nuevas: + +- `scripts/tripo-generate.js`: habla con la API y escribe el GLB. +- `scripts/tripo-viewer.js`: server local del visor. +- `scripts/tripo-viewer.html`: la pagina del visor. + +Los GLB van a `assets/tripo/` y no se commitean: el peso de salida no esta +publicado, y la notebook del demo los lleva en disco. `.gitignore` ignora +`.env`, `.env.*`, `assets/tripo/` y `vendor/model-viewer.min.js`. + +### Generar + +La key sale de `TRIPO_API_KEY` o, si no esta, del archivo +`%USERPROFILE%\.config\tripo\key.txt` (en bash, `~/.config/tripo/key.txt`). Con +el archivo, la key no queda en el historial de la terminal: + +```powershell +New-Item -ItemType Directory -Force "$HOME\.config\tripo" | Out-Null +notepad "$HOME\.config\tripo\key.txt" +``` + +```powershell +npm.cmd run tripo -- --balance +npm.cmd run tripo -- --name yelmo --dry-run "a low-poly iron helmet with a single rune on the brow, game prop" +npm.cmd run tripo -- --name yelmo "a low-poly iron helmet with a single rune on the brow, game prop" +npm.cmd run tripo -- --name cofre --image referencia.png +npm.cmd run tripo -- --name yelmo --task task_abc123 +``` + +Con npm, las opciones van despues de `--`. Sin ese `--`, npm se queda con +`--name` y `--timeout` y el script recibe solo el prompt. En bash es igual, con +`npm run`. + +| Opcion | Que hace | +| ----------------- | ------------------------------------------------------------------- | +| `--name ` | Guarda `assets/tripo/.glb`. Sin `--name`, usa el task_id. | +| `--image ` | Imagen a 3D. PNG o JPEG, hasta 20 MB. | +| `--faces ` | `face_limit`, de 50 a 20000. Default 3000 en texto, 5000 en imagen. | +| `--timeout ` | Corta la espera, de 1 a 300 segundos. En vivo, `--timeout 120`. | +| `--task ` | Retoma una tarea ya creada: espera y baja el GLB sin pagar otra. | +| `--dry-run` | Muestra el pedido y sale. No gasta creditos. | +| `--balance` | Muestra `balance` y `frozen`. No gasta creditos. | + +`--name` y `--task` aceptan letras, numeros, `_` y `-`. Una opcion mal escrita +(`--balence`, `-balance`) corta antes de crear la tarea, en vez de terminar como +texto del prompt. Antes de pagar, el script muestra en una linea el prompt, las +caras, el nombre y el corte. + +La ruta del GLB sale por stdout; el progreso y los errores, por stderr. Con +`npm run`, el encabezado de npm tambien sale por stdout: para leer solo la ruta, +usá `node scripts/tripo-generate.js` directo o `npm run -s tripo -- ...`. Si +algo falla, el codigo de salida es 1. + +Que hace con cada problema: + +- Ninguna consulta espera mas de 15 segundos y la descarga no pasa de 60. Sin + ese limite, el `fetch` de Node espera hasta 300 segundos una red colgada, y + `--timeout 120` no cortaria a tiempo. +- 429: espera `Retry-After` (o `X-RateLimit-Reset`), entre 1 y 60 segundos y + nunca mas alla del corte, y reintenta hasta 3 veces por pedido. +- Corte de red, respuesta cortada, 429 o 5xx mientras consulta la tarea: sigue + consultando hasta el corte. La tarea sigue corriendo en Tripo aunque se caiga + el wifi. +- El alta no se reintenta ante un corte de red: si el POST llego a Tripo, un + reintento podria cobrar dos veces. Antes del POST imprime + `creando la tarea...`. +- `failed`, `cancelled`, `banned` o `expired`: corta y muestra `error_code` y + `error_message`. +- Si corta despues de crear la tarea, por `--timeout`, la red o un error, + imprime el comando para retomarla con `--task` sin pagar otra. Tripo cobra la + tarea si termina, aunque el script ya haya cortado. +- Si la descarga falla o la URL vencio, vuelve a pedir la tarea por una URL + nueva, durante 5 minutos como maximo. +- Solo guarda un GLB entero: revisa que empiece con `glTF` y que el largo + coincida con el que declara. +- La descarga se escribe en `.glb.part` y se renombra al terminar, asi el + visor nunca lee un GLB a medias. En Windows reintenta el rename si el visor + justo esta leyendo el archivo anterior. + +### Ver + +```powershell +node scripts/tripo-viewer.js --fetch-lib +npm.cmd run tripo:viewer +``` + +`--fetch-lib` se corre una vez, con wifi: baja `@google/model-viewer` 4.3.1 a +`vendor/model-viewer.min.js`, fuera de git, y lo rechaza si su sha256 no +coincide. El wifi del evento no tiene que ser parte del demo. Despues, +`npm run tripo:viewer` imprime `http://127.0.0.1:4173/`: abrí esa URL. + +- Escucha solo en `127.0.0.1`, responde solo a los Host `127.0.0.1` y + `localhost`, y sirve cuatro rutas: la pagina, model-viewer, `/models.json` y + `/assets/tripo/.glb`. El resto del repo no se publica. +- Muestra los `.glb` con letras, numeros, `_` o `-` y la extension en + minuscula. A los demas los saltea y lo avisa una vez en su consola. +- La pagina consulta `/models.json` cada 3 segundos. Un GLB nuevo aparece solo, + uno regenerado con el mismo nombre se recarga y queda marcado, y uno borrado + sale de la grilla. +- Si un GLB no carga, el texto debajo del modelo lo dice. +- Los GLB con meshopt cargan sin red. Los que usan Draco o KTX2 piden su + decodificador a un CDN de Google y sin wifi no cargan; KTX2 se ve blanco, sin + error. El script no pide compresion, asi que no deberian aparecer. Para + revisarlo despues de generar: + +```powershell +node -e "const fs=require('fs');for(const f of fs.readdirSync('assets/tripo'))if(f.endsWith('.glb')){const b=fs.readFileSync('assets/tripo/'+f);console.log(f,JSON.parse(b.toString('utf8',20,20+b.readUInt32LE(12))).extensionsUsed||[])}" +``` + +Si un GLB lista `KHR_draco_mesh_compression` o `KHR_texture_basisu`, regeneralo. + +- Si el puerto esta ocupado o reservado: `$env:PORT = "4174"` antes de + arrancarlo. +- Dejá la ventana del visor a la vista: con Chrome minimizado o tapado, el + navegador frena la consulta y el yelmo puede aparecer recien al volver. +- El visor no lleva la API key. + +### Prueba sin creditos + +El 8 de octubre el script se probo contra un server falso que imita la v3: 429 +en el alta, 503 en una consulta, URL del GLB vencida en la primera descarga, +upload multipart, imagen a 3D, saldo, tarea fallida, consulta colgada con +`--timeout`, tarea retomada con `--task`, opcion mal escrita y descarga que no +es un GLB. `TRIPO_BASE_URL` apunta el script a ese server; en el demo no se usa. +La generacion real contra Tripo queda para cuando haya key (seccion 5, paso 2). + +## 5. Orden hasta el 11 + +Hoy es 8 de octubre. El escenario es el 11. Hacelo en este orden: + +1. Hoy: creá la key, guardala en el archivo de la seccion 4 (o en + `TRIPO_API_KEY`) y corré `npm.cmd run tripo -- --balance`. Si `balance` es 0, cargá creditos en la + consola. El minimo de recarga queda sin verificar. +2. Hoy: un pedido de texto real, con el yelmo: + `npm.cmd run tripo -- --name yelmo "a low-poly iron helmet with a single rune on the brow, game prop"`. + Abrilo en el visor o en Blender. Si este paso falla, frená y mirá el error + antes de gastar en los demas. +3. Con un GLB real en disco: generá `heroe`, `cofre` y `coloso` con `--name` y + los prompts de la seccion 1. Dejalos en la notebook, fuera de git. +4. `node scripts/tripo-viewer.js --fetch-lib` una vez y + `npm.cmd run tripo:viewer`. Confirmá que los tres GLB giran en el navegador + con el juego abierto en otra ventana. +5. Un ensayo completo, antes del 11: terminal, visor, y una generacion en vivo + con `--timeout 120`. Si a los 2 minutos no hay `success`, el script corta y + mostrás los tres de respaldo. Si la tarea termina despues, el comando + `--task` que dejo impreso la baja sin pagar otra. Anotá los creditos que + imprime al terminar. +6. Imagen a 3D, solo si el texto ya funciona y te sobra un rato el 9 o el 10. + Un PNG o JPEG de referencia, no una captura ASCII. No es el numero del + escenario. +7. El 11 no es para la primera llamada. Llevás la key ya probada, los GLB en + disco, el visor abierto y el juego en la version que ya corre. + +Antes del escenario, borrá de `assets/tripo/` los GLB de ensayo que no quieras +mostrar, como `prueba.glb` o un yelmo del ensayo: el visor los saca de la +grilla solo. Si dejás un yelmo viejo, en pantalla ya hay un yelmo antes del +pedido en vivo. + +No hace falta tocar tests del juego. Este corte no cambia `lib/`. + +## 6. Limites que importan el 11 + +Precios publicados el 8 de octubre de 2026. 1 credito = 0,01 USD. 100 creditos += 1 USD. + +Serie P1, en https://developers.tripo3d.ai/en/models/p1 : + +| Tarea | Sin textura | Textura standard | Textura detailed | +| ----------- | ----------- | ---------------- | ---------------- | +| Texto a 3D | 30 | 40 | 50 | +| Imagen a 3D | 40 | 50 | 60 | + +El demo usa textura standard: 40 creditos por modelo de texto (0,40 USD) y 50 +por uno de imagen (0,50 USD). Cuatro textos, tres de respaldo y uno en vivo, +son 160 creditos si todos salen bien: 1,60 USD. Deja margen para dos o tres +reintentos. + +Serie H, tabla visible en https://developers.tripo3d.ai/en/pricing : + +| Tarea | Sin textura | Textura standard | +| ----------- | ----------- | ---------------- | +| Texto a 3D | 10 | 20 | +| Imagen a 3D | 20 | 30 | + +Add-ons de esa pagina, sumados a la base: HD Texture +10, 8K Ultra Texture +20, +HD Geometry Quality +20, Quad Mesh +5, Smart Low-poly +10, Generate Parts +20. +Como mapean `detailed` y `extreme` a esas etiquetas queda sin verificar. El tab +P Series de esa misma pagina no vino en el HTML leido; los numeros P1 salen de +la ficha del modelo. + +Al crear la tarea, Tripo aparta los creditos. Si sale `success`, los descuenta. +Si falla o se cancela, los devuelve. `banned` y `expired` no dicen en la tabla +si devuelven el credito: sin verificar. Miralos con `--balance` (`balance` y +`frozen`) y en los creditos que imprime el script al terminar. + +Tiempo: + +- P1: unos 10 segundos sin textura, unos 60 con textura. +- Quick start: 10 a 120 segundos como rango general. +- Poll cada 2 segundos, corte a los 5 minutos. +- En el escenario, `--timeout 120`: a los 2 minutos volvés a los GLB de respaldo. + +Concurrencia, un solo modelo a la vez en el demo. Si lanzás varios juntos, frená +en 3: la ficha P1 dice 3 tareas P en paralelo y la pagina de rate limit dice 5 +para la serie P. La serie H figura en 10. La concurrencia es por cuenta, no por +key. Si el cupo esta lleno, HTTP 429 y `code` 2000, con header `Retry-After`. + +Rate limit, distinto de la concurrencia: por API key. La pagina de rate limit +dice HTTP 429 y `code` 1007, y publica `X-RateLimit-Limit`, +`X-RateLimit-Remaining` y `X-RateLimit-Reset`. La pagina de errores, en cambio, +describe 2000 como rate limit. El script, ante un 429, espera `Retry-After` o +`X-RateLimit-Reset` antes de reintentar. El cupo exacto de requests queda sin +verificar. + +Archivos: + +- Imagen de entrada: maximo 20 MB. PNG o JPEG para el upload. Minimo sugerido, + 256 por 256. +- Subir un modelo 3D: maximo 150 MB. No es el caso del demo. +- GLB de salida: tamano sin verificar. El script lo baja apenas termina la tarea, + porque la URL v3 vence a los 5 minutos. + +## 7. Fuera del demo + +Esto no entra antes del 11: + +- Meter el mesh en la grilla ASCII, en `lib/render.js` o en los mapas. +- Rig, retarget, multiview, refine, conversión a FBX, quad, partes sueltas. +- Un backend publico, WalletConnect, o pagar los creditos con la wallet de Runa. +- Llamar a Tripo desde el browser con la key. +- Reescribir el juego como web app. +- Commitear la key, un `.env`, o los GLB generados. +- Implementar la v2 (`POST https://api.tripo3d.ai/v2/openapi/task`). + +## 8. Sin verificar + +Verificado el 8 de octubre: la base `https://openapi.tripo3d.ai/v3` responde, y +una key invalida da HTTP 401 con `code` 2. Lo demas sigue abierto: + +- Prefijo de la API key en la v3. +- Prompts en espanol en la v3. +- `type` real de una tarea image-to-model (el ejemplo publicado dice + `text_to_model`). +- WebP en `POST /v3/files`. +- Si el GET del GLB exige `Authorization`. +- Tamano del GLB generado. +- QPS numerico de los endpoints de generacion. +- Como se apilan `texture_quality=detailed` y `extreme` sobre la tabla H. +- Si el tab P de `/en/pricing` coincide con la ficha P1. +- Si `banned` o `expired` devuelven los creditos. +- Minimo de recarga en la consola. +- Cual de los dos topes de concurrencia P aplica: 3 o 5. +- Cual de los dos rangos de caras aplica en el borde: 48 o 50. + +## 9. Fuentes + +Leidas el 8 de octubre de 2026. + +V3, la que usa el demo: + +- https://developers.tripo3d.ai/en/docs/quick-start +- https://developers.tripo3d.ai/en/docs/authentication +- https://developers.tripo3d.ai/en/docs/generation-text-to-model/standard +- https://developers.tripo3d.ai/en/docs/generation-text-to-model/p +- https://developers.tripo3d.ai/en/docs/generation-image-to-model +- https://developers.tripo3d.ai/en/docs/generation-image-to-model/p +- https://developers.tripo3d.ai/en/docs/task-query +- https://developers.tripo3d.ai/en/docs/task-lifecycle +- https://developers.tripo3d.ai/en/docs/files +- https://developers.tripo3d.ai/en/docs/billing +- https://developers.tripo3d.ai/en/docs/rate-limits +- https://developers.tripo3d.ai/en/docs/error-handling +- https://developers.tripo3d.ai/en/docs/migration-v2-to-v3 +- https://developers.tripo3d.ai/en/pricing +- https://developers.tripo3d.ai/en/models/p1 +- https://platform.tripo3d.ai (consola, API Keys) + +V2, solo para no seguirla: + +- https://platform.tripo3d.ai/docs/ +- https://platform.tripo3d.ai/docs/quick-start +- https://platform.tripo3d.ai/docs/generation +- https://platform.tripo3d.ai/docs/task +- https://platform.tripo3d.ai/docs/upload +- https://platform.tripo3d.ai/docs/limit +- https://platform.tripo3d.ai/docs/billing +- https://platform.tripo3d.ai/docs/faq +- https://platform.tripo3d.ai/docs/post-process diff --git a/lib/game.js b/lib/game.js index 175a57f..5ba9029 100644 --- a/lib/game.js +++ b/lib/game.js @@ -2829,7 +2829,8 @@ class Runa { slot: this.pendingSlot || this.slotCursor + 1, replacing: this.replacing, realms: REALMS, - realmCursor: this.realmCursor + realmCursor: this.realmCursor, + frame: Math.floor((this.animationTick * TICK_MS) / 125) } ) } @@ -2838,7 +2839,8 @@ class Runa { page: this.menuPage, slots: this.slots, cursor: this.menuPage === 'slots' ? this.slotCursor : this.menuCursor, - message: this.menuMessage + message: this.menuMessage, + frame: Math.floor((this.animationTick * TICK_MS) / 125) }) } @@ -2893,6 +2895,7 @@ class Runa { equipment, items, combat: this.duelStats(this.player.snapshot()), + frame: Math.floor((this.animationTick * TICK_MS) / 125), footer: this.inventoryHome ? inventoryFooter( autosave + 'izq/der mochila/deposito | ENTER transferir | X equipar/quitar' @@ -2914,6 +2917,7 @@ class Runa { gold: this.player.gold, items: cat.lines || [], cursor: this.cursor, + frame: Math.floor((this.animationTick * TICK_MS) / 125), loadout: { left: CONTENT.items[this.player.equipped.left_hand] || null, right: CONTENT.items[this.player.equipped.right_hand] || null, diff --git a/lib/render.js b/lib/render.js index 690c6dc..6d6d1e5 100755 --- a/lib/render.js +++ b/lib/render.js @@ -33,6 +33,16 @@ const { style } = require('bare-tui') const { makeMovingHeroSprite } = require('./sprites.js') const { WORLD_BOSS } = require('./world-boss.js') const { bossCamera } = require('./world-boss-event.js') +const TITLE_TURNTABLE = require('../assets/ascii/paisaje.json') +const HERO_TURNTABLE = require('../assets/ascii/heroe.json') +const HELMET_TURNTABLE = require('../assets/ascii/yelmo.json') +const FIELD_COLOSSUS = require('../assets/ascii/coloso-field.json') + +function turntableFrame(model, w, h, frame = 0) { + const variant = model.variants.find((size) => size.width <= w && size.height <= h) + if (!variant) return null + return variant.frames[Math.floor(Math.max(0, Number(frame) || 0)) % variant.frames.length] +} /** Smallest terminal the split layout is designed for. */ const MIN_WIDTH = 64 @@ -567,9 +577,33 @@ function titleScreen(w, h, status, menu = null) { const fits = (lines, extraRows) => h >= lines.length + extraRows && Math.max(...lines.map((line) => line.length)) + 2 <= w const slots = menu && Array.isArray(menu.slots) ? menu.slots : null - const full = fits(LOGO, slots ? 12 : 4) - const art = full ? LOGO : fits(wordmark, 4) ? wordmark : ['RUNA'] + const frameIndex = Math.floor(Math.max(0, Number(menu && menu.frame) || 0)) + const turntable = + menu && menu.page === 'main' + ? TITLE_TURNTABLE.variants.find( + (variant) => variant.width + 2 <= w && variant.height + WORDMARK_LINES + 13 <= h + ) + : null + const full = !turntable && fits(LOGO, slots ? 12 : 4) + const art = turntable + ? [ + ...wordmark, + 'EL REINO | HEROE Y COLOSO', + ...turntable.frames[frameIndex % turntable.frames.length] + ] + : full + ? LOGO + : fits(wordmark, 4) + ? wordmark + : ['RUNA'] const centre = (t) => ' '.repeat(Math.max(0, Math.floor((w - style.width(t)) / 2))) + t + const frameWidth = () => Math.min(w - 2, Math.max(42, Math.min(72, w - 8))) + const frameTop = (label, width) => { + const title = '[ ' + label + ' ]' + return '+' + title + '-'.repeat(Math.max(0, width - style.width(title) - 2)) + '+' + } + const frameLine = (value, width) => '| ' + padTo(value, width - 4) + ' |' + const frameBottom = (width) => '+' + '-'.repeat(Math.max(0, width - 2)) + '+' const tag = 'no controlas a tu personaje. escribis las reglas que sigue.' const sceneWidth = Math.max(...LOGO.slice(WORDMARK_LINES + 1).map((line) => line.length)) const painted = art.map((line, i) => { @@ -617,21 +651,40 @@ function titleScreen(w, h, status, menu = null) { { label: 'SALIR', disabled: false } ] const cursor = Math.max(0, Math.min(choices.length - 1, Number(menu.cursor) || 0)) - body.push(centre(style().bold(true).foreground('yellow').render('MENU PRINCIPAL'))) + const width = frameWidth() + body.push( + centre(style().bold(true).foreground('yellow').render(frameTop('RUNA LAUNCHER', width))) + ) + body.push( + centre( + frameLine( + style().bold(true).render('MENU PRINCIPAL') + + ' ' + + style().foreground('cyan').render('VERSION 0.2.0') + + ' ' + + style().faint(true).render('reinos | coloso | inventario'), + width + ) + ) + ) + body.push(centre(frameLine('', width))) for (let index = 0; index < choices.length; index++) { const choice = choices[index] const selected = index === cursor const line = (selected ? '> ' : ' ') + choice.label - const paint = style().faint(choice.disabled).reverse(selected) - body.push(centre(paint.render(selected ? ' ' + line + ' ' : line))) + const paint = style().faint(choice.disabled).reverse(selected).bold(selected) + body.push(centre(frameLine(paint.render(padTo(line, width - 4)), width))) } body.push( - '', centre( - style() - .faint(true) - .render('ARRIBA/ABAJO | ENTER / ESPACIO aceptar | ? controles | N nueva | Q salir') - ) + frameLine( + style() + .faint(true) + .render('ARRIBA/ABAJO ENTER / ESPACIO aceptar ? controles Q salir'), + width + ) + ), + centre(style().bold(true).foreground('yellow').render(frameBottom(width))) ) } else { body.push(centre(style().bold(true).foreground('yellow').render('PARTIDAS GUARDADAS'))) @@ -693,8 +746,10 @@ function newGameScreen(w, h, input, initial, error, options = {}) { h = Math.max(0, Math.floor(h)) const centre = (line) => ' '.repeat(Math.max(0, Math.floor((w - style.width(line)) / 2))) + style.truncate(line, w) - const sprite = heroSprite({ initial, items: [] }) const realms = Array.isArray(options.realms) ? options.realms : [] + const extraRows = 15 + realms.length + (options.replacing ? 2 : 0) + (error ? 2 : 0) + const preview = turntableFrame(HERO_TURNTABLE, w - 2, h - extraRows, options.frame) + const sprite = preview || heroSprite({ initial, items: [] }) const realmCursor = Math.max( 0, Math.min(realms.length - 1, Math.floor(Number(options.realmCursor) || 0)) @@ -1129,11 +1184,10 @@ function fieldPane(field, w, h) { } } if (boss && !boss.defeated) { - const phaseFrames = WORLD_BOSS.fieldSprite.phaseFrames[boss.phase] - const bossArt = - (phaseFrames && phaseFrames[boss.frame]) || - WORLD_BOSS.fieldSprite.frames[boss.frame] || - WORLD_BOSS.fieldSprite.frames.idle + // Reuse Tripo geometry on the existing 43x13 canvas. Telegraphs and + // hazards below still come from the original combat simulation. + const facing = boss.frame === 'punchLeft' ? 5 : boss.frame === 'punchRight' ? 7 : 6 + const bossArt = FIELD_COLOSSUS.variants[0].frames[facing] const left = Math.round(boss.x) - WORLD_BOSS.fieldSprite.anchor.x const top = Math.round(boss.y) - WORLD_BOSS.fieldSprite.anchor.y const bossColor = boss.active ? (boss.phase === 'furia' ? 'magenta' : 'red') : 'gray' @@ -1442,6 +1496,10 @@ function shopPane(shop, w, h) { } const sel = items[clamp(cursor, 0, items.length - 1)] + if (sel && sel.id === 'iron_helmet') { + const preview = turntableFrame(HELMET_TURNTABLE, w, h - lines.length - 1, shop.frame) + if (preview) lines.push('YELMO DE HIERRO', ...preview) + } if (sel && sel.about) { lines.push('-'.repeat(w)) for (const l of wrap(ascii(sel.about), w)) lines.push(style().faint(true).render(l)) @@ -1515,8 +1573,14 @@ function inventoryPane(model, w, h) { return style().width(w).height(h).render(lines.join('\n')) } + const selectedItem = items[cursor] + const preview = + selectedItem && selectedItem.id === 'iron_helmet' + ? turntableFrame(HELMET_TURNTABLE, w, h - lines.length - 6, model.frame) + : null + const previewRows = preview ? preview.length + 1 : 0 const detailRows = h >= 20 ? 4 : 0 - const visibleRows = Math.max(1, h - lines.length - detailRows) + const visibleRows = Math.max(1, h - lines.length - detailRows - previewRows) const start = clamp( cursor - Math.floor(visibleRows / 2), 0, @@ -1543,6 +1607,7 @@ function inventoryPane(model, w, h) { } const selected = items[cursor] + if (preview) lines.push('YELMO DE HIERRO', ...preview) if (selected && detailRows) { lines.push('-'.repeat(w)) const stats = [] diff --git a/package.json b/package.json index d7b56d1..f9d6f95 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "lint": "prettier . --check && lunte lib app.js index.js bin.mjs test workers scripts", "start": "bare bin.mjs --no-updates", "make": "node scripts/make.js", + "tripo": "node scripts/tripo-generate.js", + "tripo:viewer": "node scripts/tripo-viewer.js", "make:darwin-arm64": "bare-build --name runa --standalone --host darwin-arm64 --out ./out/darwin-arm64 bin.mjs", "make:darwin-x64": "bare-build --name runa --standalone --host darwin-x64 --out ./out/darwin-x64 bin.mjs", "make:linux-arm64": "bare-build --name runa --standalone --host linux-arm64 --out ./out/linux-arm64 bin.mjs", diff --git a/scripts/demo-walkthrough.js b/scripts/demo-walkthrough.js new file mode 100644 index 0000000..d20f9e4 --- /dev/null +++ b/scripts/demo-walkthrough.js @@ -0,0 +1,104 @@ +'use strict' + +// Reproducible recording source: real Runa input, ticks and renderer. +// Uses an isolated in-memory fixture; never opens a player's save store. +// bare scripts/demo-walkthrough.js output/playwright/demo +const fs = require('bare-fs') +const path = require('bare-path') +const { Runa } = require('../lib/game.js') +const { Field } = require('../lib/field.js') +const { BossZone } = require('../lib/boss-zone.js') +const { ansiToHtml, page } = require('./readme-screens.js') + +const game = new Runa({ presence: false }) +game.width = 120 +game.height = 44 +const frames = [] +const chapters = [] +const press = (name) => game.onKey({ type: 'key', is: (...keys) => keys.includes(name) }) +const type = (letter) => + game.onKey({ type: 'key', sequence: letter, ctrl: false, meta: false, is: () => false }) + +function chapter(caption, seconds, step = () => {}) { + chapters.push({ second: frames.length / 15, caption }) + for (let i = 0; i < seconds * 15; i++) { + step(i) + game.onTick() + frames.push({ caption, html: ansiToHtml(game.view()) }) + } +} + +chapter('RUNA | Tripo: reino, heroe y coloso en una escena ASCII', 8) +press('enter') +chapter('Nueva partida | Heroe generado con Tripo, nombre y reino', 8, (i) => { + if (i >= 20 && i < 24) type('Ayla'[i - 20]) +}) +press('enter') +chapter('Exploracion | El mundo y los personajes viven en la terminal', 10, (i) => { + if (i % 4 === 0) press(i < 75 ? 'up' : 'right') +}) +game.walker.placeAt('nox', 160, 103) +chapter('Recorrido de muestra | NOX, el segundo reino', 6, (i) => { + if (i % 6 === 0) press('left') +}) +game.field = new Field({ seed: 17, player: game.player }) +game.field.player.x = 111 +game.field.player.y = 10 +chapter('Pradera | Exploracion y criaturas en movimiento', 8, (i) => { + if (i % 5 === 0) press('right') +}) +game.field = null +game.walker.placeAt('city', 160, 130) +game.player.xp = 60 +game.player.gold = 100 +game.player.hp = game.player.maxHp +game.shop = 'armor' +game.cursor = 5 +chapter('Partida de muestra: nivel 3 y 100 oro | Yelmo Tripo en la armeria', 10, (i) => { + if (i === 95) press('enter') +}) +press('escape') +game.openInventory() +game.inventoryCursor = game.inventoryItems('carried').findIndex((item) => item.id === 'iron_helmet') +chapter('Yelmo comprado y equipado | ASCII animado, defensa +2', 10) +game.inventoryOpen = false +game.field = new BossZone({ + seed: 27, + player: game.player, + script: game.scriptSource, + x: 101, + y: 22 +}) +chapter('Ruinas del Coloso | El mismo modelo Tripo ahora vive en el mapa', 8) + +const directory = path.resolve(Bare.argv[2] || 'output/playwright/demo') +fs.mkdirSync(directory, { recursive: true }) +fs.writeFileSync(path.join(directory, 'frames.json'), JSON.stringify(frames)) +fs.writeFileSync(path.join(directory, 'chapters.json'), JSON.stringify(chapters, null, 2) + '\n') +const controls = `` +fs.writeFileSync( + path.join(directory, 'index.html'), + page('RUNA - Demo Tripo', game.view()).replace('', controls + '') +) +console.log(`${directory}: ${frames.length / 15}s, ${frames.length} real renderer frames`) diff --git a/scripts/glb-to-ascii.py b/scripts/glb-to-ascii.py new file mode 100644 index 0000000..be94ae6 --- /dev/null +++ b/scripts/glb-to-ascii.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""Bake a GLB into text turntables. Offline only; requires Python + numpy.""" +import argparse +import json +import math +import struct +from pathlib import Path + +import numpy as np + + +def load_mesh(file): + blob = Path(file).read_bytes() + if blob[:4] != b'glTF' or struct.unpack_from(' right or top > bottom: + continue + yy, xx = np.mgrid[top:bottom+1, left:right+1] + a = ((y1-y2)*(xx-x2)+(x2-x1)*(yy-y2))/denominator + b = ((y2-y0)*(xx-x2)+(x0-x2)*(yy-y2))/denominator + z = a*z0+b*z1+(1-a-b)*z2 + region = depth[top:bottom+1, left:right+1] + mask = (a >= -1e-6) & (b >= -1e-6) & (a+b <= 1+1e-6) & (z > region) + region[mask] = z[mask] + pixels[top:bottom+1, left:right+1][mask] = ramp[min(len(ramp)-1, int(shade*(len(ramp)-1)))] + frames.append([''.join(row) for row in pixels]) + return {'width': width, 'height': height, 'frames': frames} + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('input') + parser.add_argument('--output', required=True) + parser.add_argument('--frames', type=int, default=24) + parser.add_argument('--sizes', default='64x22,40x10,28x5', + help='Comma-separated character canvases, e.g. 43x13') + parser.add_argument('--hero', help='Compose a hero GLB in front of this landscape') + parser.add_argument('--colossus', help='Compose a colossus GLB opposite the hero') + args = parser.parse_args() + if not 2 <= args.frames <= 120: + parser.error('--frames must be between 2 and 120') + try: + sizes = [tuple(map(int, size.split('x'))) for size in args.sizes.split(',')] + if any(len(size) != 2 or not 8 <= size[0] <= 160 or not 4 <= size[1] <= 60 for size in sizes): + raise ValueError('Invalid canvas size') + except ValueError: + parser.error('--sizes expects widths 8..160 and heights 4..60, e.g. 43x13') + mesh = load_mesh(args.input) + if bool(args.hero) != bool(args.colossus): + parser.error('--hero and --colossus must be supplied together') + if args.hero: + # Reuse the actual generated meshes, positioning them on the landscape + # base. This is an offline tableau, not a skeletal combat animation. + mesh *= 10 / np.ptp(mesh[:, :, 0]) + mesh[:, :, 2] -= 3 + floor = mesh[:, :, 1].min() + 0.35 + parts = [mesh] + for file, height, x, angle in [(args.hero, 1.8, -2, math.pi/4), + (args.colossus, 4.0, 2, -math.pi/4)]: + part = load_mesh(file) + part *= height / np.ptp(part[:, :, 1]) + c, s = math.cos(angle), math.sin(angle) + part = part @ np.array([[c, 0, -s], [0, 1, 0], [s, 0, c]]) + part[:, :, 0] += x + part[:, :, 1] += floor - part[:, :, 1].min() + part[:, :, 2] += 3 + parts.append(part) + mesh = np.concatenate(parts) + mesh -= (mesh.min(axis=(0, 1)) + mesh.max(axis=(0, 1))) / 2 + result = {'source': Path(args.input).name, 'frameMs': 125, + 'variants': [bake(mesh, width, height, args.frames) for width, height in sizes]} + if args.hero: + result['composedFrom'] = [Path(args.input).name, Path(args.hero).name, Path(args.colossus).name] + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(result, ensure_ascii=True, indent=2) + '\n') + print(f'{output}: {args.frames} angles, {len(sizes)} sizes, {output.stat().st_size} bytes') diff --git a/scripts/readme-screens.js b/scripts/readme-screens.js index 897ea1c..dc07b7a 100644 --- a/scripts/readme-screens.js +++ b/scripts/readme-screens.js @@ -263,6 +263,37 @@ function frames() { const inventory = equippedGame('Ayla') inventory.openInventory() + const tripoMenu = new Runa({ presence: false }) + tripoMenu.width = WIDTH + tripoMenu.height = 44 + tripoMenu.animationTick = 12 + const colossusFrame = tripoMenu.view() + tripoMenu.animationTick = 57 + + const tripoName = new Runa({ presence: false }) + tripoName.width = WIDTH + tripoName.height = 44 + tripoName.onKey(key('enter')) + for (const ch of 'Ayla') tripoName.onKey(type(ch)) + + const tripoShop = equippedGame('Ayla') + tripoShop.height = 44 + tripoShop.player.xp = 60 + tripoShop.player.gold = 100 + tripoShop.player.hp = tripoShop.player.maxHp + tripoShop.shop = 'armor' + tripoShop.cursor = 5 + + const tripoInventory = equippedGame('Ayla') + tripoInventory.height = 44 + tripoInventory.player.xp = 60 + tripoInventory.player.gold = 100 + tripoInventory.player.hp = tripoInventory.player.maxHp + tripoInventory.openInventory() + tripoInventory.inventoryCursor = tripoInventory + .inventoryItems('carried') + .findIndex((item) => item.id === 'iron_helmet') + return { menu: { title: 'menu principal', frame: menu.view() }, nombre: { title: 'nueva partida', frame: name.view() }, @@ -277,14 +308,25 @@ function frames() { 'nox-oficios': { title: 'barrio de oficios de nox', frame: noxWorkshops.view() }, dungeon: { title: 'cripta - nivel 2', frame: dungeon.view() }, 'world-boss': { title: 'ruinas volcanicas del coloso', frame: worldBoss.view() }, - inventario: { title: 'inventario y equipo', frame: inventory.view() } + inventario: { title: 'inventario y equipo', frame: inventory.view() }, + 'tripo-coloso': { title: 'Tripo - coloso en el menu', frame: colossusFrame }, + 'tripo-heroe': { title: 'Tripo - heroe en el menu', frame: tripoMenu.view() }, + 'tripo-paisaje': { title: 'Tripo - reino, heroe y Coloso', frame: colossusFrame }, + 'tripo-coloso-mapa': { title: 'Tripo - Coloso dentro del mapa', frame: worldBoss.view() }, + 'tripo-personaje': { title: 'Tripo - creacion de personaje', frame: tripoName.view() }, + 'tripo-yelmo-tienda': { title: 'Tripo - yelmo en la armeria', frame: tripoShop.view() }, + 'tripo-yelmo-inventario': { title: 'Tripo - yelmo equipado', frame: tripoInventory.view() } } } -const output = path.resolve(Bare.argv[2] || '.readme-screens') -fs.mkdirSync(output, { recursive: true }) -for (const [name, screen] of Object.entries(frames())) { - const target = path.join(output, name + '.html') - fs.writeFileSync(target, page(screen.title, screen.frame)) - console.log(target) +module.exports = { ansiToHtml, page } + +if (require.main === module) { + const output = path.resolve(Bare.argv[2] || '.readme-screens') + fs.mkdirSync(output, { recursive: true }) + for (const [name, screen] of Object.entries(frames())) { + const target = path.join(output, name + '.html') + fs.writeFileSync(target, page(screen.title, screen.frame)) + console.log(target) + } } diff --git a/scripts/tripo-generate.js b/scripts/tripo-generate.js new file mode 100644 index 0000000..3e7c4c8 --- /dev/null +++ b/scripts/tripo-generate.js @@ -0,0 +1,363 @@ +#!/usr/bin/env node +'use strict' + +// Genera un GLB con la API v3 de Tripo y lo deja en assets/tripo/. +// Corre con Node 18+, fuera del juego: el proceso de Bare nunca ve la key. +// Contrato y limites en docs/tripo-integration.md. + +const fs = require('fs') +const os = require('os') +const path = require('path') +const { parseArgs } = require('util') + +// TRIPO_BASE_URL solo existe para probar el script contra un server falso. +const BASE = process.env.TRIPO_BASE_URL || 'https://openapi.tripo3d.ai/v3' +const MODEL = 'P1-20260311' +const OUT_DIR = path.resolve(__dirname, '../assets/tripo') +const KEY_FILE = path.join(os.homedir(), '.config', 'tripo', 'key.txt') +const POLL_MS = 2000 +const DEADLINE_S = 300 +// Sin limite, el fetch de Node espera hasta 300 s una red o una API colgada. +const REQUEST_MS = 15000 +const DOWNLOAD_MS = 60000 +const FINAL_FAILURES = ['failed', 'cancelled', 'banned', 'expired'] +const IMAGE_TYPES = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg' } + +const USAGE = `Uso: + node scripts/tripo-generate.js [--name yelmo] "prompt en ingles" + node scripts/tripo-generate.js [--name cofre] --image referencia.png + node scripts/tripo-generate.js [--name yelmo] --task + node scripts/tripo-generate.js --balance + +Opciones: + --name archivo de salida assets/tripo/.glb (default: el task_id) + --faces face_limit, entero de 50 a 20000 (default 3000 texto, 5000 imagen) + --timeout corta la espera antes de los 300 s (ej. 120 en vivo) + --task retoma una tarea ya creada: espera y baja el GLB sin pagar otra + --dry-run muestra el pedido y sale, sin gastar creditos + --balance muestra el saldo, sin gastar creditos + +Con npm, las opciones van despues de --: npm run tripo -- --name yelmo "..." +La key sale de TRIPO_API_KEY o, si no esta, de ${KEY_FILE}.` + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms))) + +let cachedKey +function apiKey() { + if (cachedKey) return cachedKey + let key = process.env.TRIPO_API_KEY + if (!key) { + try { + key = fs.readFileSync(KEY_FILE, 'utf8') + } catch { + // Sin archivo: se avisa abajo. + } + } + key = (key || '').trim() + if (!key) { + console.error(`Falta la key: TRIPO_API_KEY en el entorno o el archivo ${KEY_FILE}`) + process.exit(1) + } + // Con un salto de linea en el medio, fetch rechaza el header y lo imprime entero. + if (/\s/.test(key)) { + console.error('La key tiene espacios o saltos de linea en el medio. Copiala de nuevo.') + process.exit(1) + } + cachedKey = key + return key +} + +// Retry-After en segundos o fecha HTTP; X-RateLimit-Reset en epoch (s o ms) o en +// segundos restantes. Siempre entre 1 y 60 s. +function retryAfterMs(response) { + const after = response.headers.get('retry-after') + let ms = Number(after) * 1000 + if (!(ms > 0) && after) ms = Date.parse(after) - Date.now() + if (!(ms > 0)) { + const reset = Number(response.headers.get('x-ratelimit-reset')) + if (reset > 1e12) ms = reset - Date.now() + else if (reset > 1e9) ms = reset * 1000 - Date.now() + else if (reset > 0) ms = reset * 1000 + } + return Math.min(Math.max(ms > 0 ? ms : 5000, 1000), 60000) +} + +// fetch solo dice "fetch failed"; la causa dice si fue DNS, TLS, un reset o el corte. +const describe = (error) => + error.cause ? `${error.message} (${error.cause.code || error.cause.message})` : error.message + +// deadline acota cada pedido y cada espera por 429. El alta, el upload y el saldo +// van sin deadline: si el POST llego, abortarlo no evita el cobro. +async function tripo(url, options = {}, deadline = Infinity, retries = 3) { + const headers = { Authorization: `Bearer ${apiKey()}`, ...options.headers } + const signal = + deadline < Infinity + ? AbortSignal.timeout(Math.max(1000, Math.min(REQUEST_MS, deadline - Date.now()))) + : undefined + let response + try { + response = await fetch(url, { ...options, headers, signal }) + } catch (error) { + throw new Error(describe(error)) + } + let body = {} + try { + body = JSON.parse(await response.text()) + } catch (error) { + // Un 200 cortado a la mitad o con HTML de un proxy es un problema de red. + if (response.ok || signal?.aborted) throw new Error(`respuesta incompleta: ${describe(error)}`) + } + if (response.status === 429 && retries > 0 && Date.now() < deadline) { + const wait = Math.min(retryAfterMs(response), deadline - Date.now()) + console.error(`429 (code ${body.code}), espero ${Math.ceil(wait / 1000)} s`) + await sleep(wait) + return tripo(url, options, deadline, retries - 1) + } + if (!response.ok || body.code !== 0) { + const hint = + response.status === 401 + ? ' -- revisa que sea la API key y no el Client ID (tcli_)' + : response.status === 403 || body.code === 2010 + ? ' -- puede faltar saldo: npm run tripo -- --balance' + : '' + const error = new Error(`Tripo ${response.status} ${JSON.stringify(body)}${hint}`) + error.status = response.status + throw error + } + return body.data +} + +// Sin status es un corte de red. 429 y 5xx tambien se reintentan. +const transient = (error) => + error.status === undefined || error.status === 429 || error.status >= 500 + +async function createTextTask(prompt, faces) { + const data = await tripo(`${BASE}/generation/text-to-model`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt, model: MODEL, face_limit: faces, texture: true, pbr: true }) + }) + return data.task_id +} + +async function createImageTask(imagePath, faces) { + const type = IMAGE_TYPES[path.extname(imagePath).toLowerCase()] + const bytes = fs.readFileSync(imagePath) + if (bytes.length > 20 * 1024 * 1024) throw new Error('La imagen pasa los 20 MB.') + const form = new FormData() + form.append('file', new Blob([bytes], { type }), path.basename(imagePath)) + // Sin Content-Type a mano: fetch pone el boundary del multipart. + const uploaded = await tripo(`${BASE}/files`, { method: 'POST', body: form }) + const data = await tripo(`${BASE}/generation/image-to-model`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + input: uploaded.file_token, + model: MODEL, + face_limit: faces, + texture: true, + pbr: true + }) + }) + return data.task_id +} + +async function waitTask(taskId, deadline) { + while (Date.now() < deadline) { + let task + try { + task = await tripo(`${BASE}/tasks/${encodeURIComponent(taskId)}`, {}, deadline) + } catch (error) { + // Un corte del wifi no mata la tarea: Tripo sigue trabajando. + if (!transient(error)) throw error + console.error(`consulta fallida, reintento: ${error.message}`) + await sleep(Math.min(POLL_MS, deadline - Date.now())) + continue + } + console.error(`${task.status} ${task.progress ?? 0}%`) + if (task.status === 'success') return task + if (FINAL_FAILURES.includes(task.status)) { + const reason = task.error_message || task.error_msg || '' + throw new Error(`${task.status} ${task.error_code || ''} ${reason}`.trim()) + } + await sleep(Math.min(POLL_MS, deadline - Date.now())) + } + throw new Error(`${taskId} sigue sin success al corte`) +} + +// La doc v3 usa output.model_url; los SDK oficiales caen a estos otros campos. +function modelUrl(task) { + const output = task.output || {} + return output.model_url || output.pbr_model || output.model || output.base_model +} + +async function downloadGlb(url, dest) { + let response + try { + response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_MS) }) + } catch (error) { + throw new Error(`descarga: ${describe(error)}`) + } + if (!response.ok) throw new Error(`descarga ${response.status}`) + const bytes = Buffer.from(await response.arrayBuffer()) + // Un GLB empieza con "glTF" y declara su largo: descarta una pagina de error o + // un archivo cortado. + const glb = + bytes.length >= 12 && + bytes.toString('latin1', 0, 4) === 'glTF' && + bytes.readUInt32LE(8) === bytes.length + if (!glb) throw new Error(`descarga invalida: ${bytes.length} bytes que no son un GLB entero`) + const tmp = dest + '.part' + fs.mkdirSync(path.dirname(dest), { recursive: true }) + fs.writeFileSync(tmp, bytes) + // En Windows el rename falla si el visor esta leyendo el GLB anterior. + for (let attempt = 1; attempt <= 10; attempt++) { + try { + fs.renameSync(tmp, dest) + return + } catch (error) { + if (attempt === 10 || !['EPERM', 'EBUSY', 'EACCES'].includes(error.code)) throw error + await sleep(100) + } + } +} + +// La URL del GLB vence a los 5 minutos: si la descarga falla, se pide la tarea de +// nuevo por una URL fresca, hasta 5 minutos. +async function downloadTask(taskId, task, dest) { + const deadline = Date.now() + DEADLINE_S * 1000 + for (;;) { + try { + const url = modelUrl(task) + if (!url) throw new Error(`${taskId} termino sin output.model_url`) + return await downloadGlb(url, dest) + } catch (error) { + if (Date.now() > deadline) throw error + console.error(`${error.message}, pido la URL otra vez`) + await sleep(POLL_MS) + task = await waitTask(taskId, deadline) + } + } +} + +function integer(flag, value, min, max) { + if (value === undefined) return undefined + const n = Number(value) + if (!Number.isInteger(n) || n < min || n > max) { + throw new Error(`--${flag} tiene que ser un entero de ${min} a ${max}, no "${value}"`) + } + return n +} + +// Sin el -- de "npm run tripo -- ...", npm se queda con las opciones y las deja en +// npm_config_*: el script recibiria solo el prompt y el pedido saldria pago, +// incluso con --dry-run, que tambien es una opcion de npm. +const SCRIPT_OPTIONS = ['name', 'image', 'faces', 'timeout', 'task', 'dry_run', 'balance'] + +// strict: una opcion mal escrita (--balence, -balance) corta aca, antes de pagar, +// en vez de terminar como texto del prompt. +function parseCli(argv) { + const eaten = SCRIPT_OPTIONS.filter((key) => process.env[`npm_config_${key}`] !== undefined) + if (eaten.length) { + const flags = eaten.map((key) => '--' + key.replace('_', '-')).join(' ') + throw new Error( + `npm se quedo con ${flags}. Van despues de --: npm run tripo -- --name yelmo "..."` + ) + } + const { values, positionals } = parseArgs({ + args: argv, + strict: true, + allowPositionals: true, + options: { + name: { type: 'string' }, + image: { type: 'string' }, + faces: { type: 'string' }, + timeout: { type: 'string' }, + task: { type: 'string' }, + balance: { type: 'boolean' }, + 'dry-run': { type: 'boolean' }, + help: { type: 'boolean', short: 'h' } + } + }) + const args = { + name: values.name, + image: values.image, + task: values.task, + faces: integer('faces', values.faces, 50, 20000), + timeout: integer('timeout', values.timeout, 1, DEADLINE_S), + balance: values.balance, + dryRun: values['dry-run'], + help: values.help, + prompt: positionals.join(' ').trim() + } + if (args.help || args.balance) return args + for (const flag of ['name', 'task']) { + if (args[flag] !== undefined && !/^[\w-]+$/.test(args[flag])) { + throw new Error(`--${flag} solo acepta letras, numeros, _ y -`) + } + } + const sources = [args.prompt, args.image, args.task].filter(Boolean).length + if (sources !== 1) throw new Error('Va uno solo: un prompt, --image o --task.\n\n' + USAGE) + if (args.prompt.startsWith('/')) { + throw new Error(`"${args.prompt}" parece una opcion. Usa --help.`) + } + if (args.prompt.length > 1024) throw new Error('El prompt pasa los 1024 caracteres.') + if (args.image && !IMAGE_TYPES[path.extname(args.image).toLowerCase()]) { + throw new Error('La imagen tiene que ser PNG o JPEG.') + } + return args +} + +async function main() { + const args = parseCli(process.argv.slice(2)) + if (args.help) { + console.log(USAGE) + return + } + if (args.balance) { + const data = await tripo(`${BASE}/account/balance`) + console.log(`balance ${data.balance} frozen ${data.frozen}`) + return + } + + const faces = args.faces || (args.image ? 5000 : 3000) + const timeout = args.timeout || DEADLINE_S + const what = args.task + ? `retomar ${args.task}` + : args.image + ? `imagen ${args.image}` + : `prompt "${args.prompt}"` + console.error( + `pedido: ${what}, caras ${faces}, nombre ${args.name || 'el task_id'}, corte ${timeout} s` + ) + if (args.dryRun) return + apiKey() + + if (!args.task) console.error('creando la tarea...') + const taskId = + args.task || + (args.image + ? await createImageTask(args.image, faces) + : await createTextTask(args.prompt, faces)) + console.error(`tarea ${taskId}`) + const resume = `node scripts/tripo-generate.js --task ${taskId}${args.name ? ` --name ${args.name}` : ''}` + + try { + const task = await waitTask(taskId, Date.now() + timeout * 1000) + const dest = path.join(OUT_DIR, `${args.name || taskId}.glb`) + await downloadTask(taskId, task, dest) + if (task.credits_consumed !== undefined) console.error(`creditos ${task.credits_consumed}`) + console.log(dest) + } catch (error) { + // La tarea ya existe y Tripo la cobra si termina: se retoma sin pagar otra. + error.message += `\npara retomarla sin pagar otra: ${resume}` + throw error + } +} + +main().catch((error) => { + console.error(error.message) + // process.exit(1) despues de varios fetch rompe Node 24 en Windows con un assert + // de libuv (0xC0000409). Con exitCode el proceso sale solo, con codigo 1. + process.exitCode = 1 +}) diff --git a/scripts/tripo-viewer.html b/scripts/tripo-viewer.html new file mode 100644 index 0000000..945d482 --- /dev/null +++ b/scripts/tripo-viewer.html @@ -0,0 +1,168 @@ + + + + + + Runa / Tripo + + + + + + +
+

[ RUNA / TRIPO ]

+ Runa sigue en la terminal. Aca solo los GLB. +
+
+ + + diff --git a/scripts/tripo-viewer.js b/scripts/tripo-viewer.js new file mode 100644 index 0000000..c096ef7 --- /dev/null +++ b/scripts/tripo-viewer.js @@ -0,0 +1,137 @@ +#!/usr/bin/env node +'use strict' + +// Visor local de los GLB de Tripo, al lado de la terminal del juego. +// Solo sirve cuatro cosas: el HTML, model-viewer, la lista de GLB y los GLB. +// No toca la API ni ve la key. +// +// node scripts/tripo-viewer.js --fetch-lib baja model-viewer una vez, con wifi +// node scripts/tripo-viewer.js http://127.0.0.1:4173/ + +const crypto = require('crypto') +const fs = require('fs') +const http = require('http') +const path = require('path') + +const root = path.resolve(__dirname, '..') +const MODELS = path.join(root, 'assets/tripo') +const HTML = path.join(__dirname, 'tripo-viewer.html') +const LIB = path.join(root, 'vendor/model-viewer.min.js') +const LIB_URL = 'https://cdn.jsdelivr.net/npm/@google/model-viewer@4.3.1/dist/model-viewer.min.js' +// sha256 de ese archivo: si el CDN devuelve otra cosa, no se guarda. +const LIB_SHA256 = '283b0672384614b4847636c306fc93fe4b1fcadc76d668b4e47f0ca76bcf033b' +const PORT = process.env.PORT ? Number(process.env.PORT) : 4173 +const PREFIX = '/assets/tripo/' +// El nombre tambien protege las rutas: solo se sirve lo que cumple esto. +const GLB = /^[\w-]+\.glb$/ +const skipped = new Set() + +async function fetchLib() { + const response = await fetch(LIB_URL) + if (!response.ok) throw new Error(`model-viewer ${response.status}`) + const bytes = Buffer.from(await response.arrayBuffer()) + const sha = crypto.createHash('sha256').update(bytes).digest('hex') + if (sha !== LIB_SHA256) throw new Error(`model-viewer no coincide: sha256 ${sha}`) + fs.mkdirSync(path.dirname(LIB), { recursive: true }) + fs.writeFileSync(LIB, bytes) + console.log(LIB) +} + +// Con mtime, para que el visor recargue un GLB regenerado con el mismo nombre. +function listModels() { + let names + try { + names = fs.readdirSync(MODELS) + } catch { + return [] + } + const models = [] + for (const name of names) { + if (!GLB.test(name)) { + if (/\.glb$/i.test(name) && !skipped.has(name)) { + skipped.add(name) + console.warn(`No muestro "${name}": renombralo con letras, numeros, _ o - y .glb`) + } + continue + } + try { + models.push({ name, mtime: fs.statSync(path.join(MODELS, name)).mtimeMs }) + } catch { + // Se borro entre readdir y stat. + } + } + return models.sort((a, b) => a.mtime - b.mtime) +} + +function send(res, file, type) { + fs.readFile(file, (error, data) => { + if (error) { + res.writeHead(404).end() + return + } + res.writeHead(200, { + 'Content-Type': type, + 'Content-Length': data.length, + 'Cache-Control': 'no-store' + }) + res.end(data) + }) +} + +function serve() { + if (!Number.isInteger(PORT) || PORT < 1 || PORT > 65535) { + console.error(`PORT invalido: "${process.env.PORT}". Usa un entero de 1 a 65535.`) + process.exit(1) + } + if (!fs.existsSync(LIB)) { + console.error('Falta vendor/model-viewer.min.js: corre una vez con --fetch-lib') + } + // Solo estos Host: una pagina ajena con DNS rebinding no puede leer los GLB. + const hosts = new Set([`127.0.0.1:${PORT}`, `localhost:${PORT}`]) + http + .createServer((req, res) => { + if (!hosts.has(req.headers.host)) { + res.writeHead(421).end() + return + } + let urlPath + try { + urlPath = decodeURIComponent(req.url.split('?')[0]) + } catch { + res.writeHead(400).end() + return + } + if (urlPath === '/') return send(res, HTML, 'text/html; charset=utf-8') + if (urlPath === '/model-viewer.min.js') { + return send(res, LIB, 'text/javascript; charset=utf-8') + } + if (urlPath === '/models.json') { + res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }) + res.end(JSON.stringify(listModels())) + return + } + const name = urlPath.startsWith(PREFIX) ? urlPath.slice(PREFIX.length) : '' + if (GLB.test(name)) return send(res, path.join(MODELS, name), 'model/gltf-binary') + res.writeHead(404).end() + }) + .on('error', (error) => { + console.error( + error.code === 'EADDRINUSE' || error.code === 'EACCES' + ? `El puerto ${PORT} esta ocupado o reservado. Usa otro con la variable PORT.` + : error.message + ) + process.exit(1) + }) + .listen(PORT, '127.0.0.1', () => { + console.log(`http://127.0.0.1:${PORT}/`) + }) +} + +if (process.argv.includes('--fetch-lib')) { + fetchLib().catch((error) => { + console.error(error.message) + process.exitCode = 1 + }) +} else { + serve() +} diff --git a/test/index.js b/test/index.js index a9f3aea..c3f0e10 100644 --- a/test/index.js +++ b/test/index.js @@ -159,6 +159,62 @@ test('title screen falls back cleanly in a small terminal', (t) => { t.ok(lines.every((line) => line.length === 40)) }) +test('generated hero and helmet previews preserve character creation and equipment controls', (t) => { + const options = { realms: REALMS, frame: 0 } + const hero = style.stripAnsi(render.newGameScreen(80, 30, 'viajero', 'V', '', options)) + const rotated = style.stripAnsi( + render.newGameScreen(80, 30, 'viajero', 'V', '', { ...options, frame: 6 }) + ) + t.not(hero, rotated, 'the generated hero rotates on the creation screen') + t.ok(rotated.includes('NOMBRE viajero')) + t.ok(rotated.includes('ENTER comenzar')) + t.ok(rotated.includes('ESC volver')) + const player = new Player({ xp: 60, gold: 100 }) + const shop = { ...require('../lib/shop.js').browse('armor', player) } + shop.items = shop.lines + shop.cursor = shop.items.findIndex((item) => item.id === 'iron_helmet') + const first = style.stripAnsi(render.shopPane({ ...shop, frame: 0 }, 70, 30)) + const next = style.stripAnsi(render.shopPane({ ...shop, frame: 6 }, 70, 30)) + t.not(first, next, 'the selected helmet preview rotates in the shop') + t.ok(next.includes('YELMO DE HIERRO')) + t.ok(next.includes('75'), 'the existing purchase price remains visible') + const model = { items: [CONTENT.items.iron_helmet], cursor: 0, frame: 0 } + const inventory = style.stripAnsi(render.inventoryPane(model, 70, 30)) + t.ok(inventory.includes('YELMO DE HIERRO')) + t.ok(inventory.includes('defensa +2'), 'the equipment bonus remains visible') +}) + +test('title turntable rotates on ticks without hiding menu controls', (t) => { + const game = new Runa({ presence: false }) + for (const [width, height] of [ + [80, 44], + [80, 30], + [80, 24], + [40, 10] + ]) { + game.update({ type: 'resize', width, height }) + game.animationTick = 0 + const first = style.stripAnsi(game.view()) + game.update({ type: 'tick' }) + game.update({ type: 'tick' }) + const next = style.stripAnsi(game.view()) + t.is(next.split('\n').length, height) + t.ok(next.split('\n').every((line) => line.length === width)) + if (height >= 24) { + t.not(first, next, 'the precomputed view advances without a keypress') + t.ok(next.includes('SALIR'), 'the menu remains visible') + t.ok(next.includes('aceptar'), 'the control hint remains visible') + } + } + const menu = { page: 'main', slots: [], frame: 0 } + t.is( + render.titleScreen(80, 44, '', menu), + render.titleScreen(80, 44, '', { ...menu, frame: 24 }), + 'the landscape wraps after one rotation' + ) + t.ok(style.stripAnsi(render.titleScreen(80, 44, '', { ...menu, frame: 24 })).includes('HEROE')) +}) + test('the controls button opens a complete overlay and returns to the previous state', (t) => { const game = new Runa({ presence: false }) game.update({ type: 'resize', width: 80, height: 24 }) @@ -2074,8 +2130,12 @@ test('the world boss animates powers with real field damage', (t) => { 25 ) ) - t.ok(warningPane.includes('/___/|.[*].[*].|\\___\\'), 'warnings never overwrite the face') - t.ok(warningPane.includes('<***>'), 'warnings never overwrite the exposed core') + const tripoBoss = require('../assets/ascii/coloso-field.json').variants[0].frames[6] + t.ok( + warningPane.includes(tripoBoss[2].trim()), + 'warnings never overwrite the generated upper body' + ) + t.ok(warningPane.includes(tripoBoss[6].trim()), 'warnings never overwrite the generated torso') const phased = new WorldBossEvent({ width: 120, height: 36 }) const close = { x: phased.x - 12, y: phased.y, hp: 20 } @@ -2114,8 +2174,21 @@ test('the world boss animates powers with real field damage', (t) => { 25 ) ) - t.ok(pane.includes('[###]---\\_'), 'the camera keeps the complete left arm') - t.ok(pane.includes('_/---[###]'), 'the camera keeps the complete right arm') + t.ok(pane.includes(tripoBoss[3].trim()), 'the camera keeps the generated shoulders') + const camera = require('../lib/world-boss-event.js').bossCamera( + snap.player, + snap.boss, + snap.width, + snap.height, + 64, + 25 + ) + const lowerRow = pane.split('\n')[snap.boss.y - WORLD_BOSS.fieldSprite.anchor.y + 8 - camera.oy] + const bossLeft = snap.boss.x - WORLD_BOSS.fieldSprite.anchor.x - camera.ox + t.ok( + [...tripoBoss[8]].every((glyph, x) => glyph === ' ' || lowerRow[bossLeft + x] === glyph), + 'the camera keeps the generated lower body with transparent spaces' + ) t.ok(pane.includes('/T\\'), 'the hero remains visible while dodging') t.ok( pane.split('\n').every((line) => line.length === 64),