Arcade: give the touch pad the cartridge's own control map - #703
Merged
Conversation
The Arcade's thumb pad shipped with four arrows and a FIRE button. That is
every control the platformer has, and roughly half of the ones a Doom-format
level needs: no strafe, no sprint, no weapon switch, no automap, no way into
Doom's own menu — and no USE. E1M1's first door is thirty seconds in, and a
door is a key press, so on a phone the real Doom cartridge could be walked
that far and no further.
The pad now takes its buttons from the cartridge. Each cartridge declares a
touch profile beside the keyboard map it mirrors (`cart.touch`), and
`arcade-dock.js` fills its fixed slots from that profile through a new
`setPad`, which `startArcade` calls whenever the selected cartridge changes:
quest walk, jump
dungeon / e1m1 + back, strafe (KeyA/KeyD), RUN
doom + USE, and a tray of weapons 1-7, MAP, MENU and Enter
attract screen one round START — nothing on a title card is steerable
Geometry stays in the dock, the control map stays with the cartridge. A slot
the cartridge does not use is hidden AND loses its data-code, so a button can
never send a key the cartridge ignores. Turning rotates and strafing
translates, so the two pairs of side buttons never wear the same arrow.
Three touch behaviours the keyboard never needed:
· RUN latches instead of asking to be held — on a phone the hand that would
hold Shift is the one steering;
· presses are tracked per pointer id, so firing with one thumb while turning
with the other lifts only the key that came up, and the pressed button
captures the pointer so a thumb sliding off it still releases its key;
· pausing (and switching cartridge) releases everything the pad holds, so a
latched sprint cannot survive into the paused document.
The buttons are wired by delegation and read their code at press time, which
is what lets one set of DOM nodes follow the cartridge and lets the tray mint
its keys on the spot.
Tests: headless checks hold every profile to the keyboard map beside it —
every touch button sends a key the arcade claims while playing, a thumb can
reach every function Doom's key table gives a keyboard, turn and strafe never
share a glyph, and the code the pad labels "strafe" moves the player
perpendicular to their heading without turning them (driven through the real
raycaster simulation). Browser specs cover the dock in isolation (per
cartridge slots, the tray, a hidden slot losing its code) and the live arcade
(strafe and the RUN latch on the raycaster, USE and a weapon chip on Doom,
and the pad following a cartridge switch).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJWJUpxAvfMfopNVxtwbvW
`npm run typecheck` runs two passes, and the second (`tsconfig.tests.json`,
which type-checks the Playwright specs) rejected the new dock spec: the
`import('./arcade-dock.js')` calls inside `page.evaluate` are browser code
loading files the harness serves beside the page, but TypeScript resolves a
literal specifier from the spec's own directory and finds nothing there.
Build the specifier from a variable instead, the same way the CDN specs load
`embed.bundle.js` — the runtime string is identical and resolves against the
page, and TypeScript no longer tries to resolve it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJWJUpxAvfMfopNVxtwbvW
JSv4
pushed a commit
that referenced
this pull request
Sep 6, 2026
A clean auto-merge, so the changelog placement check ran on its own merits rather than to settle a conflict — the same reason it ran for v12.0.1. It confirms one copy of each [Unreleased] entry with this branch's REDLINE THEATER entry inside the section (line 47, ahead of the next released heading at 88). The missing blank line before "### Fixed" is main's own shape, not something this merge introduced, so it stays as main wrote it. Verified but not rebuilt and not re-measured. #703 touches the arcade demo's own JavaScript, its node checks and a new mobile spec — no npm/src, so the embed bundle the redline demo loads is byte-identical, and nothing near the diff, save or conversion paths the published figures describe. The shared surfaces are the changelog, docs/demo/README.md and the demo-logic test list, which is why the checks still ran: 61 redline node checks (45 theater, 16 stress), the arcade and engine-pin checks main just changed, and 14 browser assertions.
JSv4
added a commit
that referenced
this pull request
Sep 6, 2026
Moves the accumulated `### Changed` (the Arcade's per-cartridge touch pad, #703) and `### Fixed` (the editor block drag handle clipped to the editor's own viewport, #702) out of `[Unreleased]` under a dated 12.1.0 heading. A `### Changed` section is present, so this is a minor bump rather than a patch. Also restores the blank line between the `### Changed` block and the `### Fixed` heading, which #703's entry had closed up — the release body copies those sections verbatim, and a heading butted against the previous paragraph does not render as a heading. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SxehJkp2547AyP8LSd6uFS
JSv4
pushed a commit
that referenced
this pull request
Sep 6, 2026
Main cut v12.1.0, moving the accumulated "### Changed" (#703) and "### Fixed" (#702) out of [Unreleased] under a dated heading. Textually that move is just a heading inserted above entries this branch also carries, so git auto-merged it with three insertions and no conflict — and left this branch's "### Added" sitting underneath the new "## [12.1.0]" heading, filed into a version that has already shipped, with [Unreleased] empty above it. That is exactly the failure the placement check exists to catch, and it caught it: the check prints the REDLINE THEATER line only while it is still inside [Unreleased], and after the auto-merge it printed nothing. The entry is moved back under [Unreleased]; the released section keeps main's two entries, in main's order, with the blank line main restored before "### Fixed". The diff against main is now purely additive — this branch adds its own entry and touches nothing of main's. Worth stating plainly: a clean auto-merge is not evidence the changelog is right. This is the second release cut in a row where the sweep earned its keep, and the first where the damage was invisible in the merge output. The demo pin needs no move yet. All eleven references across the seven pages still read docxodus@12.0.1, so they agree and the pin check passes; jsDelivr answers 404 for 12.1.0 because Publish is still in flight. Re-pinning waits for main's own re-pin and a 200. Verified: 61 redline node checks (45 theater, 16 stress), 3 pin checks, 14 browser assertions. Nothing rebuilt or re-measured — the merge carries changelog prose only.
JSv4
pushed a commit
that referenced
this pull request
Sep 7, 2026
…asuring Main's range 6f140f0..2a1e6f9 is mostly two large-but-inert changes — #728 corrects misattributed Microsoft copyright headers across ~200 post-fork files, and #721 adds the ASCII DOOM showcase — plus the tail of the history archive work. npm/package.json conflicted in the usual shape (both sides extending pretest and test:demo-logic) and was resolved as a verified union: main's doom-ascii copy and check alongside this branch's four redline entries, split on " && ", diffed list against list, and revalidated with a JSON parse. The demo logic suite is eight checks now. Two things in that range could move the published diff figures, so this merge re-measured rather than assuming: the WASM project now passes -Oz to the SDK's Binaryen post-link pass, and #721 re-recorded docxodus.aotprofile (1.34 MB to 1.65 MB). Profile-guided AOT is exactly what halved these numbers in #653, so both deserved a reading rather than a guess. The figures do not move, and the table stays put for the third time. A pooled reading — two controlled fixed-input sessions plus a three-depth stress run — came out at 46 / 56 / 103 ms with ratios 32x / 41x / 89x. That is the band the README already records for its second session (41 / 55 / 101 at 34x / 45x / 80x), not a new one, and two of three ratios held. Only full + HTML crossed the noise threshold, and it moved UP while its absolute moved down — the recording path got quicker too, from about 2 ms to 1.3 ms, so the deepest pipeline merely failed to keep pace. That is the opposite of a faster-engine signature. The README gains a paragraph recording the reading, because "a size-optimizing link pass and a wider AOT profile did not move the ratio" is worth knowing precisely because both sound like they should. One correction to this branch's own recent history: the last few merge messages said they rebuilt "through the full pretest". They did not. pretest runs the demo checks, typechecks and copies already-built artifacts into dist/wasm; npm run build is what compiles the WASM engine and the bundles. dist/wasm/Docxodus.wasm was dated 2026-09-06T01:05 — the #702 merge — so #703, v12.1.0 and all three history batches were verified against that engine rather than a fresh one. The specs passed, but they did not test what those messages claimed. This merge runs the real build: Docxodus.wasm 3.47 MB to 3.80 MB, framework total up 708 KB, which is the history subsystem and the wider AOT profile arriving at last. The measurement above is therefore of everything since #702, not of this range alone, and it is reported that way. Verified on the genuinely rebuilt engine: 61 redline node checks, 3 pin checks, 14 browser assertions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The Arcade plays well on a desktop and only partly on a phone. The thumb pad shipped with four arrows and a round FIRE button — which is every control the platformer has, and about half of the ones a Doom-format level needs.
Missing on touch: strafe, sprint, weapon switching, the automap, Doom's own menu, and — decisively — USE. Freedoom's E1M1 puts its first door about thirty seconds in, and a door is a key press (
E). A phone had no key to press, so the real Doom cartridge could be walked that far and no further.What changed
The pad's buttons now come from the cartridge instead of being fixed in the dock. Each cartridge declares a touch profile beside the keyboard map it mirrors (
cart.touch), andarcade-dock.jsfills its fixed slots from that profile through a newsetPad, whichstartArcadecalls on every cartridge change:←/→,▲, round JUMP↺ ↻), strafe (◀ ▶), FIRE, RUN⏎Geometry stays in the dock; the control map stays with the cartridge, next to the keyboard
controlsline it has to agree with. A slot the cartridge does not use is hidden and loses itsdata-code, so a button can never send a key the cartridge ignores. Turning rotates (↺ ↻) and strafing translates (◀ ▶), so the two pairs of side buttons — one row apart on the same D-pad — never wear the same arrow. Doom's rarer keys ride in a tray above the action cluster rather than crowding the game screen, because they are what you reach for between fights rather than during one.Mechanism
Three touch behaviours the keyboard never needed:
aria-pressedshowing which.The pad is wired by delegation and reads each button's code at press time. That is what lets one set of DOM nodes follow the cartridge and lets the tray mint its keys on the spot — a listener that captured a code at boot would go on sending the previous cartridge's key.
Validation
Headless checks (
docs/demo/tools/ascii-arcade.test.mjs, run bynpm run test:demo-logic) hold every profile to the keyboard map beside it:0xa2missing, which is the bug this PR fixes;Browser specs (
npm/tests/demo-arcade-mobile.spec.ts, phone-shaped project) cover the dock in isolation — each cartridge's slots, the tray opening and being withdrawn, a hidden slot losing its code — and the live arcade: strafe and the RUN latch on the raycaster, USE and a weapon chip on Doom, the latch not surviving a pause, and the pad following a cartridge switch. The attract-screen assertion rides the existing intro spec.Locally I ran the headless suites (green) and exercised the dock and its event path in a real Chromium on a Pixel 5 viewport — profile switching, tray, latch, release-on-pause, tray chips. The Playwright specs themselves need the WASM bundle, which this container cannot build (no .NET SDK), so they run for the first time in CI.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WJWJUpxAvfMfopNVxtwbvW
Generated by Claude Code