These pages are generated from the project's internal documentation. References to source files and binary offsets are kept as provenance — so you can see where each claim comes from — but they are not followable yet: the engine is not published. Once it is, they will become links.
4 · The debug panel
The full QA drawer, section by section.
How to open it: the `
`key (backquote) or F4, or ?debug=1in the URL. And from ⚙ → "Debug (QA)" → "Open debug menu". It exposeswindow.__u5debug` for the console and for e2e.DEV only. The SYSTEM menu row and the shortcut are announced only when the
openDebugdependency exists: in the production faithful/shader skins nobody is told this panel is there.New screenshots produced for this document on 2026-08-04 against a dedicated dev server. They live in
docs/verdicts/debug-drawer/.
The whole panel

A 360 px right-hand drawer, in plain DOM, with a field search box at the top and an accordion of sections below.
Census, measured live on 2026-08-04
| # | Section | Fields |
|---|---|---|
| 0 | Shortcuts | 3 |
| 1 | Teleport | 7 |
| 2 | Party | 23 |
| 3 | Resources | 9 |
| 4 | World / Clock | 15 |
| 5 | Plot / Quest | 45 |
| 6 | Transports | 7 |
| 7 | NPCs (dead / met) | 65 |
| 8 | Dungeon · cleared rooms | 17 |
| 9 | Moonstones | 32 |
| 10 | Inventory · Reagents | 8 |
| 11 | Inventory · Equipment (stock) | 48 |
| 12 | Inventory · Spells | 48 |
| 13 | Inventory · Potions / Scrolls | 16 |
| Total | 14 sections · 343 fields |
The four architectural rules
Before the tour, because they explain almost everything you see.
1 · Zero-rand, by construction
Every write goes through a single facade (debug/debugApi.ts) that writes straight into live state — the same struct that gets persisted to the native save — and never through Intents or anything that consumes the RNG stream. Each method is a handful of flat writes plus a refresh().
Why it matters: if the panel consumed rolls, using it would change the game invisibly, and any comparison against DOS would be contaminated. The invariant is asserted by a test (debug-api.test.ts) checking that liveSeed() does not move after any operation.
Even the teleport respects the rule: it replicates the deep-link recipe by setting position and, on small maps, enterMap + hydrateInteriorObjects — both deterministic.
2 · The registry is declarative; the panel knows nothing
registry.ts declares what can be edited ({label, get, set, widget}); panel.ts draws it. Adding a new field is one more entry in the registry, without touching the panel.
3 · It does not get in the game's way
The canvas stays playable with the drawer open, and the panel does not capture the game's keys except when one of its inputs has focus (stopPropagation only for events originating in the panel). Refresh is per field: get() is re-read and the control updated without rebuilding the DOM, so the teleport map and the focus survive.
4 · No cap is invented
An explicit mandate of the original brief. Every limit is derived from the field type in SAVED.GAM and the cap the game applies, or from a formula already derived from the binary, and it is cited in the constant:
| Cap | Value | Where it comes from |
|---|---|---|
| Gold / food / experience | 9999 | u16 fields, but the game saturates them at CAP_WORD (add_word_capped, kernel 0x9C84) |
| Keys, gems, torches… | 99 | u8 fields, but the model treats quantities as two-digit values |
| Karma | 99 | u8 at 0x2e2, two digits in Ztats |
| STR / DEX / INT | 30, not 99 | ⬅️ see below |
The 30 is the pretty detail. The turn scheduler and the binary's thresholds operate as CONST − stat in unsigned bytes: a stat above 0x23 (35) wraps. With DEX 99 the party ends up immobile and untouchable — the game "works", and it is broken. So the panel's cap is 30: genuinely fast and inside the regime. The field label says so, and the derivation is in the internal record audit-byte-wrap.
It is the perfect example of why a state editor needs to know the binary: a control with max=99 would have been "correct" according to the data type and would have produced an irreproducible bug.
Section 0 · Shortcuts

Three one-click buttons, right at the top because they are what gets used most:
| Button | What it does |
|---|---|
| Max everything | Party (HP/MP/stats/level/status) + resources + inventory to their caps + all special items (grapple, LB regalia, shards, spyglass, sextant, watch, badge, box, HMS Cape) |
| Best equipment for everyone | Equips each member with the best item per slot, derived from the attack/defence tables in the data — not from a hand-written list — respecting the two-handed rule |
| Full party at max | Fills the party with real roster members (up to 6) and applies the other two |
All three are pure logic in debug/shortcuts.ts (no mutation of Game, no DOM), and the facade applies them. All zero-rand.
Existing verification: docs/verdicts/debug-maxall/.
| Shards and regalia | Badge and specials |
|---|---|
![]() | ![]() |
Section 1 · Teleport

Two routes: the dropdowns (location + floor, or dungeon) and — the good one — the map picker.
The map picker

A large centred modal, because cramming a map into a 360 px drawer is not usable. Three tabs:
- Overworld — 256×256 with zoom (wheel), pan (drag) and double-click to zoom in; Britannia / Underworld layer.
- Cities and castles — 32×32 grid, with a floor selector when the location has several.
- Dungeons — the whole 8×8 floor, no fog (it is a debug tool), in the gem's visual language: wall = white block, room = yellow box, stairs = H. Eight floor tabs.
One click teleports to that exact cell. There is also numeric x,y entry + Enter. Hovering shows (x,y) and the tile name, and in a dungeon the subtype (e.g. "Room #8"). It remembers the last destination.
The detail that decides whether it can be trusted
Every map is rendered from LIVE core data, never from stale JSON. The town grid comes from getActiveMap(world, loc, floor).tileAt — the same source the game uses to decide where you can walk. A picker fed by an exported JSON would be a map of another version of the world, and its errors would look like game bugs.
Two UX decisions
- If the destination is a wall or impassable, it warns but allows: this is debug, and sometimes you do want to land inside a wall.
- ESC closes the modal without reaching the game (captured at the root), except inside its own inputs, where you need to be able to type.
And the one action marked dangerous
The section has an "Enter dungeon (real flow)" button with a red badge:
"REAL dungeon entry — may consume RNG from the stream (not zero-rand)."
It is the declared exception to rule 1, and the panel marks it visually instead of hiding it. A state editor that does not distinguish "write a byte" from "run the game's flow" is an editor you cannot trust to set up a comparison.
Screenshots of teleporting to the five destination types: docs/verdicts/debug-teleport/ (01-overworld, 02-overworld-zoom, 03-underworld, 04-town, 05-dungeon), and of the tiling and the gem fix in docs/verdicts/debug-picker-tiling/ and docs/verdicts/debug-picker-gemfix/.
| Before the gem fix | After |
|---|---|
![]() | ![]() |
Section 2 · Party (23 fields)

A character selector and, per member: name, gender (0x0B/0x0C), class (the nine letters of "AMBFDTPRS"), status (G/P/C/S/D), HP, max HP, MP, level, experience, STR/DEX/INT, partyStatus, months at the inn and every equipment slot.
The labels carry the warning attached: "Strength (max 30 — >35 wraps the scheduler, audit-byte-wrap)". The derivation travels with the control, not in a separate document nobody will open.
Also: party size (1..6) and active character.
Section 3 · Resources (9 fields)

Gold, food, keys, gems, torches, skull keys, magic carpets, karma and the other global counters. Each with its derived cap (§"No cap is invented").
Section 4 · World / Clock (15 fields)

Year, month (with the six real names: Deep Winter, Awakening, Time of Sowing, Season of Sun, Time of Harvest, Fruitful Winter), day, hour, minute, wind (Calm/N/S/E/W), transport, and the runtime fields the save persists:
- turns since start, torch minutes,
- time effect (
timeSpell) and its turns, - light minutes (
lightSpell), prevHour(snapshot),- latched Felucca and Trammel phases.
That the two latched moon phases are editable is not a whim: they are what determines where a moongate takes you, and without them you cannot set up the scenario for a moon-travel comparison.
Section 5 · Plot / Quest (45 fields)
Words of power spoken (×8), shadowlords killed (×3), in-doom, game-won, Blackthorn… and every live key in questFlags, raw, including the ones nobody knows how to derive (marked "underived").
Total enumeration, not curated — an explicit mandate from the user. The difference matters: a curated list only shows the flags someone already understood, which are exactly the ones you least need to inspect.
Trade-off: you can write a flag nobody understands
The price of enumerating everything is that the panel offers editable controls over underived keys. You can see them — which is the point — but you can also write them, and writing a bit whose meaning is unknown produces a state the game may not know how to read, with no warning at all.
The section marks them "underived", and that label is all the protection there is. It is consistent with the rest of the panel (this is a QA tool, not a fool-proof interface), but it is worth knowing that the noise is not only in the reading.
Section 6 · Transports (7 fields)

transportTile as a raw byte, plus the frigate and wind state: hull, skiffs, sail direction, wind-drift counter, HMS Cape toggle. And a QA button to empty the enemy pool.
Existing verification: docs/verdicts/debug-save-editor/01-transportes.png.
Section 7 · NPCs (dead / met) — 65 fields
The largest section: by (location, NPC index), a location dropdown and 32 + 32 checkboxes (dead / met).
⚠️ Careful: the index is not the position in an array — it is an index belonging to the NPC. Confusing them means marking the neighbour instead.
Existing verification: docs/verdicts/debug-save-editor/03-npcs-dead.png.
Section 8 · Dungeon · cleared rooms (17 fields)

The bitmap of already-cleared rooms: 7 slots × 16 rooms.
And an oddity of the binary that the panel reflects instead of papering over: Deceit and Despise collapse into slot 0. The eight dungeons share seven slots. The label says so — "Deceit / Despise" — and the derivation lives in dungeonClearedBitIndex.
An editor that had "fixed" that by giving eight slots would have written bits the game does not read.
Existing verification: docs/verdicts/debug-save-editor/02-mazmorra-salas.png.
Section 9 · Moonstones (32 fields)

Position and state of the eight moonstones.
Sections 10-13 · Inventory (120 fields)
| Section | Fields | Screenshot |
|---|---|---|
| Reagents | 8 | ![]() |
| Equipment (stock) | 48 | ![]() |
| Spells | 48 | ![]() |
| Potions / Scrolls | 16 | ![]() |
Every cell is a numeric field labelled with the item's real name, read from InventoryDetails.json. There are no bare indices forcing you to consult a separate table.
The guard that stops this falling behind
The save editor keeps a coverage manifest — COVERED_STATE_KEYS and EXCLUDED_STATE_KEYS — which is the source of a completeness test: if someone adds a new field to GameState and does not put it in either list, the guard goes red.
That is the difference between a debug panel that ages well and one that six months later edits 70% of the state with nobody knowing which 70%. The decision it forces is not "add the field to the panel", but the cheaper and more useful one: decide and declare whether it travels or is excluded.
What this panel is not
- It is not a save editor for players. It is QA. It sits behind a key that is not advertised in production.
- It does not replace the save panel (F5, 3 · Experience §5): that one faces the player, exports and imports, and edits nothing.
- It is not entirely zero-rand: "Enter dungeon (real flow)" can consume the stream, and it says so in red.







