Estas páginas se generan a partir de la documentación interna del proyecto. Las referencias a ficheros de código y a offsets del binario se conservan como procedencia —para que se vea de dónde sale cada afirmación—, pero todavía no se pueden seguir: el motor aún no está publicado. Cuando lo esté, pasarán a ser enlaces.
3 · Mejoras de experiencia
Controles táctiles, tocar el mapa para caminar, ranuras de guardado, el español y el historial con scroll.
Los cinco puntos están confirmados contra el código, no interpretados. Al final del documento hay una sección sobre lo que se comprobó y cómo. También hay una mejora retirada y un defecto vivo encontrado hoy que no estaban en el encargo.
Índice
- Tocar el mapa para caminar ✨
- Controles táctiles: el deck ✨
- El censo que faltaba: cinco comandos inalcanzables ✨
- El bug del «on press» ✨
- Múltiples ranuras de guardado ✨
- El español ✨
- El historial con scroll ✨
- Lo que se QUITÓ: el diario y el minimapa 🗑️
- Un defecto vivo encontrado escribiendo esto
- Qué se verificó y cómo
1 · Tocar el mapa para caminar
✨ AÑADIDO · Código: game/src/ui/autowalk.ts, game/src/core/world/pathfind.ts, skin/fiel/skin.ts (emisión del intent)
Antes
Flechas del teclado, un paso por pulsación. Sin teclado —o sea, en un móvil— quedaba la cruceta de la botonera, un toque por casilla.
Ahora
Se toca una casilla del visor y el grupo camina hasta ella. Un paso cada 140 ms, siguiendo el camino que calcula un A* (findPath) sobre el mapa activo y respetando el medio de transporte (a pie, a caballo, alfombra, esquife, barco: el mismo transport que usa el motor).
La decisión de diseño que lo hace seguro
Cada paso pasa por game.move(dir) y applyEvents — exactamente el mismo camino que una pulsación de flecha. No hay una ruta paralela que teletransporte, ni un atajo que se salte los eventos del turno. Es la diferencia entre «una comodidad» y «una segunda implementación del movimiento que se desincroniza».
Y se cancela ante:
- cualquier comando manual,
- combate (hallazgo R1 del carril: caminar solo en combate era exactamente lo que no se quería),
- cambio de mapa,
- y
"Blocked!"— si el camino se corta a mitad, la marcha para.
Contrapartidas
- El paso es de 140 ms fijo. No acelera en trayectos largos. Cruzar Britannia tocando el borde del visor es lento a propósito: cada paso es un turno del juego, con sus encuentros y su reloj.
- Sin camino, no pasa nada.
findPathdevuelvenully la función es un no-op silencioso: tocar una montaña no da mensaje de error. Es discreto, pero también es indistinguible de un toque no registrado. - Es QoL, no fidelidad. El original no tenía ratón. Está en el mismo saco que el resto de este documento.
2 · Controles táctiles: el deck
✨ AÑADIDO · Código: game/src/ui/touch.ts (fuente única de la botonera), skin/portrait/deck-ancho.ts (CSS), deck-nativo.ts (comportamiento)

La botonera aparece cuando el puntero es grueso (pointer: coarse) — se mide el puntero, no el user-agent. Y no es un criterio elegido para esto: es el mismo predicado con el que aparece el layout partido y con el que la piel decide su encaje móvil. Atarlos garantiza que el layout partido y la botonera que lo hace usable aparezcan siempre juntos.
Botones, medidos en vivo el 04-08-2026:
| Hoja | Botones |
|---|---|
Mundo (WORLD_BUTTONS) | 25 |
Utilidad (UTIL_BUTTONS) | 3 |
Mazmorra (DUNGEON_BUTTONS) | 5 |
Combate (COMBAT_BUTTONS) | 2 |
El menú ☰

Seis entradas: ES (idioma) · Piel · Sistema · Cambiar el pad de lado · Layout: partido / original · Cerrar.
«Cambiar el pad de lado» mueve la cruceta entre izquierda y derecha; el defecto es derecha, «para los diestros» (deck-nativo.ts:ladoGuardado). Y «Layout: partido / original» es el ▤ del que habla 2 · Layouts §4.
La pista de que hay más abajo
Los ~14 comandos bajo el pliegue eran invisibles: «no parece que haya más». La solución es un chevron con degradado en el borde correspondiente cuando hay contenido fuera de la vista (ui/scroll-hint.ts).
Detalle que evita un bug clásico: los dos overlays llevan pointer-events: none, así que jamás roban un tap. Y la lógica (scrollHintState) es pura, con un épsilon de 2 px porque scrollTop llega con fracciones cuando el DPR no es 1.
Contrapartida: medir el puntero también acierta de más
Atarse a pointer: coarse es lo correcto —el user-agent miente y un criterio propio dejaría el layout partido sin sus botones—, pero tiene un coste que conviene saber: un portátil con pantalla táctil da coarse. Ese usuario tiene teclado y ratón, y aun así recibe el deck y el layout partido por defecto.
No es un fallo del predicado: es que la pregunta «¿hay dedos?» y la pregunta «¿es esto un móvil?» no son la misma, y sólo la primera se puede medir. La salida existe y es de un clic —el ▤ del menú ☰, o ⚙ → Vídeo— y la preferencia manda sobre el defecto en los dos sentidos, así que el que lo apaga se queda apagado. Pero el primer arranque en ese equipo se ve como un móvil.
3 · El censo que faltaba: cinco comandos inalcanzables
✨ AÑADIDO · Acta: movil-cmds-acta (28-07-2026)
La pregunta del usuario
«El comando ignite torch está en los comandos botones de móvil? haz revisión de que tengamos todos los comandos en la lista de botones.»
Cómo se contestó, que es lo interesante
No con una lista de memoria, sino leyendo el despachador real: kernel_cmd_dispatch @0x3178, caso por caso, más la tabla de saltos de 'F'..'L' en 0x3490. Eso da el conjunto completo de comandos, no el que alguien recordaba.
De paso salieron dos no-comandos: 'D' y 'W' imprimen su propio «D-What?»/«W-What?», y el default en 0x34d8 cierra la tabla con «What?».
El resultado
No, «Ignite torch» no estaba: existía sólo en la hoja de MAZMORRA. De noche a la intemperie no había forma de encender una antorcha sin teclado físico. Y con él faltaban otros cuatro:
| Comando | Tecla | Dónde estaba |
|---|---|---|
| Attack | A | sólo en COMBATE — en pueblo/overworld, inalcanzable |
| Ignite torch | I | sólo en MAZMORRA — a la intemperie, inalcanzable |
| New Order | N | en ningún sitio |
| Quit & Save | Q | en ningún sitio (el «Save» del deck es F5 = panel multi-partida, no el flujo fiel por consola) |
| View a gem | V | en ningún sitio |
Los cinco se añadieron al final de WORLD_BUTTONS, sin reordenar lo que el usuario ya tenía memorizado bajo el dedo. Y se añadieron a una sola tabla: los decks de vertical y apaisado la consumen a través de TouchControls, no tienen copia propia.

Por qué importa más allá del arreglo
Cinco comandos de veinticinco estaban fuera de alcance para cualquiera sin teclado, y la lista llevaba tiempo pareciendo completa. Lo que la destapó no fue una revisión: fue comparar contra la fuente de verdad en vez de contra el recuerdo de quien la escribió.
Contrapartida: la lista ya no tiene un orden defendible
Añadirlos al final protege la memoria muscular de quien ya usaba el deck —el motivo por el que se hizo así— pero el precio es que la rejilla deja de tener una lógica que un recién llegado pueda seguir. Atacar y Encender antorcha, que son de los comandos más frecuentes, quedan al fondo, por debajo del pliegue, detrás de Prestar o Abordar.
Es la elección correcta para los usuarios que ya estaban y la peor para los que llegan. Reordenar la rejilla es un cambio de producto pendiente, no un olvido: la alternativa era mover los botones bajo el dedo de alguien que ya los tenía situados.
Y un bug de presentación del mismo carril
| antes | después |
|---|---|
![]() | ![]() |
Al pulsar espacio, el original crea una entrada de log con su >; el port ponía líneas sin > y pegadas unas a otras.
4 · El bug del «on press»
✨ AÑADIDO (arreglo de un añadido) · Código: game/src/ui/tap-or-drag.ts, deck-nativo.ts
El problema
Los botones del deck disparaban su comando en pointerdown: apoyabas el dedo y ya estaba hecho, sin poder abortar deslizando fuera. En cuanto la zona de comandos se hizo scrolleable por toque, cualquier intento de arrastre nacía sobre un botón y disparaba el comando de debajo.
El arreglo
TapGate: se registra el pointerdown, se sigue el pointermove, y en pointerup se decide. Es un tap sólo si el puntero no se desplazó más de 10 px (distancia de Chebyshev). Diez píxeles ≈ el touch-slop de Android/iOS: el temblor natural del dedo no cancela el tap, un arrastre deliberado sí.
Y cubre el caso que se olvida: cuando el navegador se queda el gesto para su pan nativo, emite pointercancel — el gate lo traduce a «no era tap».
La excepción deliberada
La cruceta se queda en pointerdown. Andar quiere respuesta inmediata y repetición al mantener; exigir el pointerup haría el movimiento esponjoso. Además la cruceta no vive en una zona scrolleable, que es de donde venía el problema. Es la única excepción y está escrita como tal.
Y cómo se hizo sin tocar ui/
Un interceptor en fase de captura sobre el deck corta el pointerdown antes de que llegue al botón, y el módulo pone su propio disparo en pointerup. Cero líneas modificadas en ui/touch.ts.
5 · Múltiples ranuras de guardado
✨ AÑADIDO · Código: game/src/core/persistence.ts, ui/savepanel.ts · F5
Antes
El original tenía un único SAVED.GAM. Está dicho en la primera línea del docstring de persistence.ts, y es la razón de ser del panel.
Ahora

Ranuras con nombre, ilimitadas, cada una con su lugar, su turno y su fecha, y botones de Cargar y Borrar. Debajo: nombrar y Guardar, más Exportar, Exportar .GAM e Importar.
Las cuatro decisiones que hay detrás
localStorage, no OPFS. OPFS sería más limpio para blobs grandes, pero su API es asíncrona y aún desigual entre navegadores.localStoragees síncrono, universal y de sobra para unGameStateserializado.- Índice ligero aparte. El índice vive en
u5clone:savesy cada partida completa enu5clone:save:<id>, para que listar las ranuras no obligue a deserializar todos los estados. - Nunca se lanza por cuota llena. El hallazgo del soak fue que un
QuotaExceededErrorsin capturar en el autoguardado del arranque dejaba el juego medio inicializado — un boot zombie. AhorasaveGamedevuelve{ok:false, reason:"quota"}y el llamante decide: mensaje al usuario en el guardado manual, silencio y log en el autoguardado. - Autoguardado rotatorio de 3 ranuras (
autosave-1/2/3, round-robin) para que un autoguardado no pise al anterior.
El puente con el original, que es lo elegante
«Journey Onward» sigue funcionando. El menú de portada del original recarga SAVED.GAM verbatim; aquí SAVED.GAM = la ranura con mayor timestamp, sea un autoguardado o un guardado manual. Sin ninguna, arranca partida nueva — igual que el SAVED.GAM por defecto que traía el disco. La multi-ranura se añade sin romper el flujo de 1988.
Exportar en formato de 1988
«Exportar .GAM» produce un SAVED.GAM + SAVED.OOL + sidecar en el formato de disco original, a partir de una plantilla de 4192 B. Se puede sacar una partida del port y —en principio— llevarla al juego real.
Contrapartidas
localStoragese puede vaciar sin avisar, y una app instalada en la pantalla de inicio arranca con partición de almacenamiento propia: las partidas del navegador no viajan a ella. (Ese mismo mecanismo fue el que hizo que el usuario viera «la versión antigua» al instalar el port en el iPhone — ver 2 · Layouts §4.)- El
.GAMnativo depende de dos plantillas de assets. Si faltainit.ool, se exporta con bloques a cero: degradación declarada, no un fallo silencioso.
6 · El español
✨ AÑADIDO · Código: game/src/i18n/ · Cambio en vivo con el botón 🌐 / ES
| Español | Inglés |
|---|---|
![]() | ![]() |
El modelo, que es lo que lo hace seguro
El inglés es el suelo del calco, no «un idioma más»: todo el texto sale byte-exacto del binario y contra él están ancladas todas las guardas (pixeldiff, Grand Tour, approved-strings). Un idioma ≠ inglés es una tabla de sustitución paralela, indexada por el string inglés original, resuelta en un único punto de estrangulamiento (t()):
lang === 'en'⇒t()es la identidad estricta. Ni una llamada altera la salida; el calco y las guardas quedan intactos.lang !== 'en'⇒t(str)devuelvetabla[str] ?? str. Un idioma incompleto degrada a inglés string a string, lo que permite aterrizarlo por lotes.
Esa última propiedad es la que hace que traducir un juego calcado byte a byte no sea un riesgo: lo que falta no rompe, se ve en inglés.
Dónde vive la tabla, y por qué ahí
En src/i18n/ (código fuente versionado), no en game/assets/. assets/ es material extraído del binario, propiedad de EA y fuera de git; la tabla de idioma es contenido autorado nuevo, y debe versionarse.
Estado, medido el 04-08-2026
| Métrica | Valor |
|---|---|
Entradas en es.json | 4 000 |
Marcadas como revisadas (reviewed: true) | 3 994 |
| Comillas del idioma | « … » |
⚠️ Ojo con el docstring. La cabecera de i18n/index.ts sigue diciendo «una SEMILLA de 20 strings (
es.json) para probarlo de punta a punta». Eso describía el alcance de la fase F1 y hoy está caducado por dos órdenes de magnitud. Las 4 000 de arriba están contadas en vivo sobre el fichero; el docstring no.
Hay una capa aparte para el shell (menús, paneles: texto autorado, no del binario) con su propia función ts().
Contrapartidas
- Los nombres de piel no se traducen — son nombres propios. Sólo el prefijo «Piel».
- El cuerpo de los letreros sigue en latín por defecto aunque el DOS lo pinte en runas; ver 1 · Imagen §8.
- Quedan huecos. Uno, encontrado escribiendo este documento, está en §9.
7 · El historial con scroll
✨ AÑADIDO · Código: game/src/skin/fiel/skin.ts (ConsoleScrollback, consoleScrollLines, consoleScrollToLive)
Antes
La consola del original muestra las últimas líneas y nada más. Lo que se sale por arriba se ha ido: no hay forma de releer qué dijo un NPC hace tres turnos.
Ahora
| Consola viva (líneas 36-40) | Modo historial (líneas 06-11) |
|---|---|
![]() | ![]() |
Rueda o arrastre sobre el área de consola y se entra en modo historial. Fíjate en la banda que aparece en el chrome: >HISTORIAL<, con los mismos remates de banda que el resto del marco de 1988. No es un scrollbar moderno pegado encima; es un indicador dibujado en el lenguaje de la piel.
Se sale volviendo al fondo o con cualquier tecla: nunca te quedas atrapado en el pasado mientras el juego espera un comando.
Los detalles que lo hacen no estorbar
- Por zonas, sin colisiones. La rueda sobre la consola desplaza el historial; sobre el panel con una lista de Ztats abierta, desplaza la lista; sobre el visor, nada (ahí vive el tocar-para-caminar). Fuera de una lista, la rueda sobre el panel ni se captura — la piel fiel queda intacta.
- El arrastre «tira del papel»: hacia abajo revela lo viejo, en las dos zonas.
- El resto sub-línea se acumula. Un arrastre lento no se pierde:
restguarda la fracción entrepointermoves y sólo aplica líneas enteras. - Byte-idéntico en reposo. Con
consoleScroll = 0el render es el de siempre. La mejora no cuesta un píxel cuando no se usa. - La piel shader lo reenvía. No lo reimplementa: captura los eventos de su canvas y llama a
consoleScrollLinesde la fiel que aloja.
Contrapartida
El historial es de la sesión, no de la partida: es estado de la piel, no del GameState. Recargar la página lo pierde.
8 · Lo que se QUITÓ: el diario y el minimapa
🗑️ RETIRADO · y (minimapa), 26-07-2026
Esta sección es la que hace honesto el resto del documento. El port tuvo dos comodidades muy cómodas y las quitó a propósito:
| Qué | Tecla | Por qué se fue |
|---|---|---|
| Diario (registro de charlas) | F6 | El original no tiene diario en ninguna tecla: getkey @0x1D5E mapea F1..F10 a 0xC9..0xD2, que caen en el «What?» del despachador. Era un popup ajeno al UI original. |
| Minimapa (con niebla de guerra) | Tab | El original sólo enseña el mapa por (V)iew con la gema. El panel de Tab regalaba gratis y sin gema lo que el original cobra. |
Los dos son veredicto explícito del usuario, del mismo día.
Lo que se conservó, y por qué
state.journalystate.exploredsiguen existiendo, vacíos, por compatibilidad del formato de guardado: una partida vieja se sigue cargando.defaultTileColorsobrevivió mudándose a src/debug/tile-colors.ts — el volcado de mapas del drawer de QA la reutiliza. La retirada se llevó el panel, no una función útil que resultó tener otro consumidor.
La lección que dejan
Un port de esta clase tiene dos formas de mejorar el juego, y son opuestas: añadir lo que el original no podía dar, y quitar lo que le roba su economía. El minimapa era comodísimo. También convertía en decorativo un objeto por el que el juego te hace pasar media aventura.
⚠️ Aviso para quien lea
docs/verdicts/shell-8x8/censo.md: ese censo lista todavía el «Panel de diario (ui/journal.ts, F6)» y el «Panel de minimapa (ui/minimap.ts, Tab)» como componentes vivos del shell. Los dos ficheros ya no existen, y no hay teclas F6 ni Tab. El censo es de antes de la retirada y no se actualizó.
9 · Un defecto vivo encontrado escribiendo esto
No estaba en el encargo, salió mirando la captura del español de §6.
Compara las dos primeras líneas de la consola en español:
>Look-Norte ← «Look» EN INGLÉS
Veis adoquines
>Buscar- ← «Search» traducido
Cancelado.
El eco del verbo de (L)ook sale sin traducir mientras el de (S)earch sí se traduce, y la dirección («Norte») también.
La causa, verificada en la tabla
"Look-" → SIN ENTRADA ⇒ t() cae al inglés
"Look" → "Mirar" ⇒ existe, pero NO es la clave que se emite
"Search-" → "Buscar-" ⇒ existe con el guion, y por eso funciona
La clave que se emite en tiempo de ejecución es la compuesta, con guion. Está la constante "Look" y falta la viva "Look-". Un fallo de una línea en la tabla, invisible a cualquier recuento de cobertura que mire las constantes del código en vez de lo que imprime la consola.
No lo he arreglado: este carril es de documentación, y meter una entrada en es.json toca el corpus vigilado. Queda reportado.
10 · Qué se verificó y cómo
Los cinco puntos del encargo se confirmaron leyendo el código, no infiriéndolos:
| Punto | Verificado en | Veredicto |
|---|---|---|
| Controles táctiles | ui/touch.ts (tablas contadas en vivo), deck-nativo.ts, tap-or-drag.ts | ✅ Correcto |
| Tocar el mapa para caminar | ui/autowalk.ts (A*, 140 ms, cancelaciones), emisión del intent en skin/fiel/skin.ts | ✅ Correcto |
| Ranuras de guardado múltiples | core/persistence.ts (índice + slots + autosave ×3), ui/savepanel.ts; y en vivo: dos ranuras creadas y listadas | ✅ Correcto — el docstring confirma que el original tenía un SAVED.GAM |
| El español (i18n) | i18n/index.ts (modelo), es.json (4 000 entradas contadas hoy) | ✅ Correcto |
| El historial con scroll | skin/fiel/skin.ts (ConsoleScrollback + rueda + arrastre por zonas); y en vivo: A/B con 40 líneas numeradas | ✅ Correcto |
Lo tomado de un acta y no re-verificado: el censo de comandos del despachador ASM de §3 (fuente: movil-cmds-acta) — no he releído kernel_cmd_dispatch yo. Lo que sí he contado hoy son las 25 entradas de WORLD_BUTTONS, que es el resultado del arreglo.
Tres afirmaciones documentadas que resultaron caducadas y están señaladas donde tocan: el docstring de i18n («20 strings»), el censo shell-8x8 (diario y minimapa vivos) y la propia claim de ese censo de que el shell se ve vectorial.
Y la comprobación del génesis
Las imágenes de estos documentos llevan arte de EA. La exclusión del repositorio público está verificada corriendo el génesis de verdad, no leyendo el script; el resultado está en el resumen de entrega del carril y en README.





