Public: anyone can read this; the crew can change it.
forked from trevato/cs · proposes
better escape handling. on mac, it keeps minimizing my window #180
Check · Review · Land · Live done — Landed and live in production.
Escape no longer costs the window. The cause is that Escape is the browser's key before it is the page's, and on macOS its meaning is *give the window back* — full screen drops out with the whole animation, mid-round — which no keydown handler can refuse. There is exactly one way to ask for the key instead of its meaning, and the new `src/engine/escape.ts` is the three lines of it: the Keyboard Lo…
shipped production is serving it deploy ae07c2d
Merged main and pushed. The only conflict was in README.md, where both sides had added a paragraph at the same point in Playing: main's radar paragraph from #176/#179 and this branch's paragraph about Escape. Both are kept — the Escape paragraph first, since it continues the full-screen thread of the sentence above it, then the radar. Everything else auto-merged cleanly, and the two places where the branches touched the same lists came out with both meanings intact: CS_KEYS now carries KeyT: "flash" from main alongside KeyP: "pause", and the key hint on the game page reads G throw an HE · T throw a flashbang · … · Esc or P pause.
Bundles rebuilt after the merge (app.c520f46081.js, 90451 B raw / 40817 B gzip, inside main's raised ceiling), bun test is 803 pass / 0 fail, and the merged app boots with / answering in 6 ms and /range in 1 ms.
Escape is asked for, not obeyed: src/engine/escape.ts holds the key through the Keyboard Lock API while the stage has the screen, so in Chrome and Edge a pause is a pause and full screen stays; in Safari and Firefox the pause panel says the screen went and Resume — a gesture — puts it back, and P pauses without ever costing the screen. /range does the same, with Capture the mouse as the way back.
🤖 Generated with Claude Code
Escape no longer costs the window. The cause is that Escape is the browser's key before it is the page's, and on macOS its meaning is give the window back — full screen drops out with the whole animation, mid-round — which no keydown handler can refuse. There is exactly one way to ask for the key instead of its meaning, and the new
src/engine/escape.tsis the three lines of it: the Keyboard Lock API, asked for when the stage takes the screen and released when it loses it,heldread live as the grant and still being full screen, and the press handed to the game whether or not the lock was granted. It knows no game — the window, the document andnavigator.keyboardarrive as a host, and aScreenis anything with a liveon, so aFullscreenis one — and a browser without the API (Safari, Firefox) getssupported: falserather than a promise nobody kept.In the shooter, Escape closes the buy menu if it is open and otherwise pauses: the panel up, the pointer let go — under the lock the browser does not do that for us, which is the point — and Resume focused. The panel's line says what the key actually cost on the browser it turned out to be: it kept the screen, or it took it and Resume will put it back. Resume is the gesture that does put it back, because the page now remembers the screen a player asked for rather than the one the browser is giving it.
Ppauses the same way and is a key no browser has a meaning of its own for, so it never costs the screen anywhere. At/rangeEscape gives the mouse back and keeps the screen where it can, and Capture the mouse takes back what a browser took.Worth looking at in the preview: press <kbd>F</kbd>, then <kbd>Esc</kbd> — in Chrome or Edge the match pauses and you stay full screen (hold Escape to leave, which is the platform's own way out); in Safari the pause says the screen went and Resume brings it back in one click.
/rangeis the same two paths. What I did not do: nothing forces full screen back without a gesture, because a browser will not honour that, and the world still simulates while paused — that is how this game already worked and is a separate change.Tests:
bun test, 781 pass. New behaviour has tests that fail without it —src/engine/escape.test.ts(nine, including the stale grant that must not outlive its screen), four insrc/client/main.test.tsand one insrc/range/boot.test.ts; the headless page grewFakeKeyboardand akeyboardLock: falseoption so the Safari path is driven, not assumed. No dependencies added; bundles rebuilt and under their ceilings (app 86 KB raw / 39 KB gzip).🤖 Generated with Claude Code
Closes #178