The references to source files and binary offsets you'll see here are the provenance of each claim: they say where it comes from. The engine is published under GPL-3.0-or-later at github.com/kokoima/openu5, so you can go and look them up one by one.
3 · Experience improvements
Touch controls, tapping the map to walk, save slots, Spanish, the scrollable log, and the developer console, and the atlas your browser builds.
Every point in this document is confirmed against the code, not inferred.
Contents
- Tapping the map to walk ✨
- Touch controls: the deck ✨
- Multiple save slots ✨
- Spanish ✨
- The scrollable log ✨
- The developer console ✨
- The atlas your browser builds ✨
1 · Tapping the map to walk
✨ ADDED · By tapping or clicking a cell in the viewport · Code: game/src/ui/autowalk.ts, game/src/core/world/pathfind.ts, skin/fiel/skin.ts (intent emission)
You tap a cell in the viewport and the party walks there, along the same code path an arrow key takes.
Before
Arrow keys, one step per press. Without a keyboard — that is, on a phone — you were left with the D-pad on the button deck, one tap per tile.
Now
You tap a cell in the viewport and the party walks there. One step every 140 ms, following the path computed by A* (findPath) over the active map and respecting the means of transport (on foot, horse, carpet, skiff, ship: the same transport the engine uses).
The design decision that makes it safe
Every step goes through game.move(dir) and applyEvents — exactly the same path as an arrow-key press. There is no parallel route that teleports, and no shortcut that skips the turn's events. That is the difference between "a convenience" and "a second implementation of movement that drifts out of sync".
And it is cancelled by:
- any manual command,
- combat (walking on your own during combat was precisely what nobody wanted),
- a map change,
- and
"Blocked!"— if the path is cut halfway, the walk stops.
Trade-offs
- The step is a fixed 140 ms. It does not speed up on long journeys. Crossing Britannia by tapping the edge of the viewport is slow on purpose: every step is a game turn, with its encounters and its clock.
- No path, nothing happens.
findPathreturnsnulland the function is a silent no-op: tapping a mountain gives no error message. That is discreet, but it is also indistinguishable from a tap that did not register. - It is quality of life, not fidelity. The original had no mouse. It is in the same bucket as the rest of this document.
2 · Touch controls: the deck
✨ ADDED · On its own, where the pointer is coarse (pointer: coarse) · Code: game/src/ui/touch.ts (single source for the button deck), skin/portrait/deck-ancho.ts (CSS), deck-nativo.ts (behaviour)
A touch deck with every command in the game, censused against the binary, that fires as the finger lifts and appears when the pointer is coarse.

The deck appears when the pointer is coarse (pointer: coarse) — the pointer is measured, not the user agent. And that is not a criterion picked for this: it is the same predicate that brings up the split layout and that the skin uses to decide its mobile fit. Tying them guarantees that the split layout and the buttons that make it usable always appear together.
Buttons, derived from the tables in the code on 2026-08-16 (the figures are checked by game/tests/dungeon-botonera.test.ts, so they cannot go stale again):
| Sheet | Buttons |
|---|---|
World (WORLD_BUTTONS) | 25 |
Utility (UTIL_BUTTONS) | 3 |
Dungeon (DUNGEON_BUTTONS) | 18 |
Combat (COMBAT_BUTTONS) | 13 |
Complete against the original, not against memory
The deck did not come from a list written from memory: it was censused against the binary's real dispatcher (kernel_cmd_dispatch @0x3178, case by case, plus the 'F'..'L' jump table at 0x3490), and the contextual sheets against the arena's own turn loop and the dungeon's per-location gate. Every command the original accepts has a button — lighting a torch at night in the open, attacking in a town or viewing a gem no longer require a physical keyboard.
The price, stated: the commands the census added go at the end of the grid, so as not to move the buttons that existing deck users had memorised under their thumb. Attack and Ignite torch, among the most frequent, sit below the fold.
The trigger, as the finger lifts
Buttons decide on pointerup, not on touch: it is a tap only if the finger moved no more than 10 px (≈ the Android/iOS touch slop), and if the browser keeps the gesture for its native pan, it does not count. Swiping to see more commands no longer presses the button underneath. The only exception is the D-pad, which stays on pointerdown: walking wants immediate response and repeat-on-hold, and it does not live in a scrollable area.
The ☰ button: the SYSTEM menu one tap away

The deck's ☰ opens the SYSTEM menu directly — the same surface as F10 and the ⚙ button. There used to be an intermediate five-entry popover here and it was retired after being measured: reaching the saves, the video or the keys always took two taps, and the first one decided nothing. Its entries live in the menu itself today: Switch pad side (moves the D-pad; the default is right, "for right-handers", deck-nativo.ts:ladoGuardado) and Split layout (portrait) — the same toggle as the ▤ discussed in 2 · Layouts §4.
The hint that there is more below
The commands below the fold were invisible: "it does not look like there is more". The solution is a chevron with a gradient on the relevant edge when there is content out of view (ui/scroll-hint.ts). Both overlays carry pointer-events: none, so they never steal a tap.
Trade-off: measuring the pointer also over-fires
Tying yourself to pointer: coarse is right — the user agent lies, and a bespoke criterion would leave the split layout without its buttons — but it has a cost worth knowing: a laptop with a touchscreen reports coarse. That user has a keyboard and a mouse, and still gets the deck and the split layout by default.
It is not a fault of the predicate: it is that "are there fingers?" and "is this a phone?" are not the same question, and only the first one can be measured. The way out exists and is one click away — the deck's ▤, or the ⚙ → Video checkbox — and the preference beats the default in both directions, so whoever turns it off stays off. But the first launch on that machine looks like a phone.
3 · Multiple save slots
✨ ADDED · Code: game/src/core/persistence.ts, ui/savepanel.ts · F5
Named, unlimited slots, with export and import, instead of the original's single SAVED.GAM.
Before
The original had a single SAVED.GAM. It is stated in the first line of the docstring of persistence.ts, and it is the reason the panel exists.
Now

Named slots, unlimited, each with its place, its turn and its date, plus Load and Delete buttons. Below: name and Save, plus Export, Export .GAM and Import.
The four decisions behind it
localStorage, not OPFS. OPFS would be cleaner for large blobs, but its API is asynchronous and still uneven across browsers.localStorageis synchronous, universal and more than enough for a serialisedGameState.- A light index kept separately. The index lives in
u5clone:savesand each full save inu5clone:save:<id>, so that listing the slots does not force deserialising every state. - It never throws on a full quota. The soak test found that an uncaught
QuotaExceededErrorin the boot autosave left the game half-initialised — a boot zombie. NowsaveGamereturns{ok:false, reason:"quota"}and the caller decides: a message to the user on a manual save, silence and a log on the autosave. - A rotating 3-slot autosave (
autosave-1/2/3, round-robin) so one autosave does not overwrite the previous one.
The bridge back to the original, which is the elegant part
"Journey Onward" still works. The original's title menu reloads SAVED.GAM verbatim; here SAVED.GAM = the slot with the highest timestamp, whether an autosave or a manual save. With none, it starts a new game — just like the default SAVED.GAM that shipped on the disk. The multi-slot feature is added without breaking the 1988 flow.
Exporting in the 1988 format
"Export .GAM" produces a SAVED.GAM + SAVED.OOL + sidecar in the original disk format, from a 4192-byte template. You can take a game out of the port and — in principle — carry it to the real game.
Trade-offs
localStoragecan be cleared without warning, and an app installed to the home screen starts with its own storage partition: browser saves do not travel to it. It is the same mechanism that makes installing the port on the home screen seem to bring back an older version — see 2 · Layouts §4.- The native
.GAMdepends on two asset templates. Ifinit.oolis missing, it exports with zeroed blocks: declared degradation, not a silent failure.
4 · Spanish
✨ ADDED · Code: game/src/i18n/ · Switched live with the 🌐 / ES button
The whole game text in Spanish, as a substitution table over the English of the transcription, switchable live.
| Spanish | English |
|---|---|
![]() | ![]() |
The model, which is what makes it safe
English is the floor of the replica, not "one more language": every piece of text comes byte-exact from the binary, and all the guards (pixeldiff, Grand Tour, approved-strings) are anchored against it. A language other than English is a parallel substitution table, keyed by the original English string, resolved at a single choke point (t()):
lang === 'en'⇒t()is strict identity. Not one call alters the output; the replica and the guards stay intact.lang !== 'en'⇒t(str)returnstable[str] ?? str. An incomplete language degrades to English string by string, which allows landing it in batches.
That last property is what makes translating a byte-for-byte replica not a risk: what is missing does not break, it shows in English.
Where the table lives, and why there
In src/i18n/ (versioned source), not in game/assets/. assets/ is material extracted from the binary and kept out of git; the language table is newly authored content and belongs under version control.
Status, measured on 2026-08-04
| Metric | Value |
|---|---|
Entries in es.json | 4 000 |
Marked as reviewed (reviewed: true) | 3 994 |
| Language quotes | « … » |
There is a separate layer for the shell (menus, panels: authored text, not from the binary) with its own ts() function.
Trade-offs
- Skin names are not translated — they are proper nouns. Only the "Skin" prefix is.
- Sign bodies are painted in runes and the log keeps Latin script. DOS paints signs in runes with the digraphs collapsed and the port does the same (game/src/skin/fiel/sign-box.ts:148, runic unless you object); the text still reads in Latin script in the console log, in whichever language you have set.
5 · The scrollable log
✨ ADDED · Wheel or drag over the console · Code: game/src/skin/fiel/skin.ts (ConsoleScrollback, consoleScrollLines, consoleScrollToLive)
The console keeps a scrollback: wheel or drag to reread what scrolled off, with the >HISTORY< band of the 1988 frame.
Before
The original's console shows the last lines and nothing more. What scrolls off the top is gone: there is no way to re-read what an NPC said three turns ago.
Now
| Live console | History mode |
|---|---|
![]() | ![]() |
Wheel or drag over the console area and you enter history mode. Notice the band that appears in the chrome: >HISTORY<, with the same band brackets as the rest of the 1988 frame. It is not a modern scrollbar stuck on top; it is an indicator drawn in the skin's own language.
You leave by returning to the bottom or with any key: you never get stuck in the past while the game waits for a command.
The details that keep it out of the way
- Zoned, no collisions. The wheel over the console scrolls the history; over the panel with a Ztats list open, it scrolls the list; over the viewport, nothing (that is where tap-to-walk lives). Outside a list, the wheel over the panel is not even captured — the faithful skin stays untouched.
- Dragging "pulls the paper": downwards reveals the old, in both zones.
- The sub-line remainder accumulates. A slow drag is not lost:
restkeeps the fraction betweenpointermoves and only applies whole lines. - Byte-identical at rest. With
consoleScroll = 0the render is the same as always. The improvement costs not one pixel when it is not being used. - The shader skin forwards it. It does not reimplement it: it captures the events on its own canvas and calls
consoleScrollLineson the faithful skin it hosts.
Trade-off
The history belongs to the session, not the save: it is skin state, not GameState. Reloading the page loses it.
6 · The developer console
✨ ADDED · ` ` key or F4, or ⚙ → "Debug (QA)" · Code: game/src/debug/ (panel, section registry, window.__u5debug` facade), gate in game/src/main.ts
The project's own debug panel, open to everyone: edit almost any field of your game — and editing marks the save as touched, forever.
Before
The original ships nothing like this. Cheating a 1988 game meant editing SAVED.GAM byte by byte with an external tool, with no safety net: one wrong byte and the save does not load.
Now
The very panel the port is debugged with travels in the published game, enabled by default (the emergency exit is ?debug=0 in the URL). It is a side drawer with a field search box and an accordion of sections: point-and-click teleport with a map picker, party and stats, resources and inventory, the Britannia clock, quest flags, NPCs, moonstones, transports. It writes directly into live state without consuming the engine's RNG — editing does not desync the game's determinism.
On mobile there is no backquote and no F4: the entry point is ⚙ → "Debug (QA)" → "Open debug menu", always available.

The drawer over the game's right-hand edge, on the desktop.
![]() | ![]() |
|---|
On the phone: the way in through ⚙ → «Debug (QA)» and the same drawer, full width.
Trade-off
It is deliberate and visible: editing marks the save. The first mutation that goes through the panel stamps a persistent mark into the save (it travels with it and survives save/load) and lights a discreet on-screen tag — ⚑ EDITED. Opening the panel and looking does not mark; editing does, and the mark cannot be removed from the panel itself. A touched game stays distinguishable from a cleanly played one — by design.
![]() | ![]() |
|---|
The same screen before and after closing the drawer: the ⚑ EDITED badge stays.
7 · The atlas your browser builds
✨ ADDED · ⚙ → "Help · Atlas & guide", or straight to /companion · Code: docs/manual/companion/byo-runtime.mjs (the client-side build), menu probe in game/src/ui/shell/companion.ts
A full guide — walkthrough, bestiary, spell and reagent tables, navigable maps — that stores none of the game's data: the maps, the interiors and the portraits are drawn by your browser, from your copy.
Before
The original shipped its documentation in the box: the cloth map, the spell book and the Book of Lore. Anyone playing from a digital copy today rarely has any of that at hand, and what circulates online are scans of material nobody is entitled to hand out.
Now
The port brings its own atlas, served at /companion and linked from inside the game at SYSTEM → Help · Atlas & guide. The link only appears if the probe answers, so it never leads to a page that is not there.
What is inside is split in two halves, and the split is the whole point:
- The half the project writes — walkthrough, bestiary, spell and reagent tables, lore, scrolls, credits — travels with the site: 832 KB of our own, carrying not one byte of the original.
- The half that is not ours — drawn maps, interiors, sprites, portraits, the dialogue — is simply not in there: your browser builds it from the files you supply at
/byo, by dragging the folder or a.zip. Nothing is uploaded anywhere: the data ends up in your own browser's store and a Service Worker serves it.
Supply nothing and the atlas does not break: it still hands you the written half in full, and marks the sections that need data with a door to /byo instead of a blank page.

The atlas with a copy supplied: the map is drawn by the browser, tile by tile.
![]() | ![]() |
|---|
The half the project writes — here, the eight reagents — needs no copy of yours; on the right, the atlas on the phone.
Trade-off
Two of them, both deliberate:
- Without your copy you get half a book, not a worse book. The data-backed sections are marked for what they are; they are not padded out with an approximation or with material pulled from somewhere else.
- The atlas does not read your save. It opens in another tab, it does not know where you are and it will not point you at your next objective: it is reference material, like the manual in the box. Working out where to go from a conversation is the game, and an arrow would replace it.









