Ask before leaving the page while a world is open (Ctrl+W closed the game)

Ctrl (replace) + W (fly) hits the browser's close-tab shortcut, which a page
cannot block. A beforeunload prompt now catches it. Autosave on hiding the
tab no longer depends on the audio context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 19:47:27 -04:00
co-authored by Claude Opus 5.5
parent fec4f3bfe1
commit 97c5ca4668
3 changed files with 24 additions and 1 deletions
+15
View File
@@ -4,6 +4,21 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-23: Ctrl+W no longer closes the game without asking (Claude Code)
Note: "crash: hoplding crtl and pressing W crashes the whole browser."
- Cause: Ctrl+W is the browser's close-tab shortcut (the whole window if it is the last tab);
holding Ctrl for replace while pressing W to fly hits it. Chrome and Brave reserve it, so the
page never sees the key and cannot cancel it.
- `src/ui/input.js`: a `beforeunload` handler makes the browser ask before leaving while
`GAME.running`. The `visibilitychange` autosave no longer depends on the audio context
existing (it used to return early before any sound had played).
- `test/play.py`: 2 checks (leaving asks while a world is open, not on the title screen); 120
checks pass. All 11 headless suites pass; `t_smooth.py` passed on its rerun (the known flaky
half-resolution check failed once at load ~5).
- Open: the real Ctrl+W prompt can only be seen in a headed browser; the owner checks it.
## 2026-10-23: build menu hover popup, top-left card hidden while building (Claude Code, with Codex) ## 2026-10-23: build menu hover popup, top-left card hidden while building (Claude Code, with Codex)
Notes: "mouse hover reviels a new info popup…" and "dont need the short description in the build Notes: "mouse hover reviels a new info popup…" and "dont need the short description in the build
+5 -1
View File
@@ -69,7 +69,11 @@ canvas.addEventListener('wheel', e => {
}, { passive: false }); }, { passive: false });
window.addEventListener('resize', () => { if (R.canvas) resizeRender(); }); window.addEventListener('resize', () => { if (R.canvas) resizeRender(); });
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.hidden && GAME.running) saveWorld('auto', true);
if (!AU.ctx) return; if (!AU.ctx) return;
if (document.hidden) { if (SET.muteUnfocused) AU.ctx.suspend(); if (GAME.running) saveWorld('auto', true); } if (document.hidden) { if (SET.muteUnfocused) AU.ctx.suspend(); }
else AU.ctx.resume(); else AU.ctx.resume();
}); });
// Ctrl+W closes the tab and the page cannot stop it, and Ctrl (replace) + W (fly) is an easy slip:
// while a world is open, ask the browser to confirm leaving
window.addEventListener('beforeunload', e => { if (GAME.running) { e.preventDefault(); e.returnValue = ''; } });
+4
View File
@@ -27,6 +27,10 @@ with sync_playwright() as p:
ev = pg.evaluate ev = pg.evaluate
look = sys.argv[2] if len(sys.argv) > 2 else 'pixel' look = sys.argv[2] if len(sys.argv) > 2 else 'pixel'
check('a new game starts fully zoomed in', ev('SET.zoom') == 12, ev('SET.zoom')) check('a new game starts fully zoomed in', ev('SET.zoom') == 12, ev('SET.zoom'))
# Ctrl+W (Ctrl to replace, W to fly) closes the tab; with a world open the browser must ask first
leave = "(() => { const e = new Event('beforeunload', { cancelable: true }); dispatchEvent(e); return e.defaultPrevented; })()"
check('leaving the page with a world open asks to confirm', ev(leave))
check('but not on the title screen', not ev(f'(() => {{ GAME.running = false; const r = {leave}; GAME.running = true; return r; }})()'))
ev("SET.zoom = 4; resizeRender(); R.snapNext = true;") # the rest of this play-through was laid out at zoom 4 ev("SET.zoom = 4; resizeRender(); R.snapNext = true;") # the rest of this play-through was laid out at zoom 4
ev(f"SET.look = '{look}'"); pg.wait_for_timeout(200) ev(f"SET.look = '{look}'"); pg.wait_for_timeout(200)
check(f'the {look} look is drawing', ev('R.smoothOn') == (look == 'smooth'), ev('[SET.look, R.smoothOn, SM.ok]')) check(f'the {look} look is drawing', ev('R.smoothOn') == (look == 'smooth'), ev('[SET.look, R.smoothOn, SM.ok]'))