Every run gets a door out: the quit chip and the LEAVE THE MATCH? ask

"all tennis screens are missing the ability to exit the game and return to
the menu — right now I need to force close the app." A quiet x chip rides the
top-right corner of every run (the scoreboard owns the left; the desktop key
hint shifts inboard). Tapping it — or Escape on desktop — freezes the world
with the existing paused flag and asks LEAVE THE MATCH?: BACK TO THE MENU in
the menu's crimson, PLAY ON underneath. Quitting is the death path minus the
death — restartRun, refreshSplash, splash — and banks nothing: an abandoned
run never happened, statistically. Verified live both ways: PLAY ON resumes
the frozen rally, quitting lands on the splash with the chip retired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-14 12:07:10 +02:00
1 parent 1facc954e2
commit 11ca935ad0
2 files changed
+46 -3

No files matched your search

+24 -3
View File
@@ -78,9 +78,22 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
html { height: 100vh; }
canvas { height: calc(100vh - env(safe-area-inset-top, 0px)); }
}
/* desktop key hint — up in the empty top-right corner now that the rack owns the bottom edge */
#hint { position: fixed; top: 6px; right: 10px; text-align: right;
/* desktop key hint — top-right, shifted left of the quit chip that owns the corner now */
#hint { position: fixed; top: 6px; right: 52px; text-align: right;
font: 400 10px Silkscreen, monospace; color: #6b7688; }
/* THE DOOR OUT (Oliver: every tennis screen lacked an exit — force-closing the app was the only
way back to the menu). A quiet chip during play; tapping it freezes the world and asks. */
#quit { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 8px); right: 8px; z-index: 5; }
#quit button { width: 34px; height: 34px; background: rgba(14,23,20,0.72); border: 2px solid var(--line);
color: var(--dim); font: 400 15px Silkscreen, monospace; cursor: pointer;
-webkit-tap-highlight-color: transparent; }
#qAsk { position: fixed; inset: 0; z-index: 6; background: rgba(0,0,0,0.55);
display: flex; align-items: center; justify-content: center; }
#quit[hidden], #qAsk[hidden] { display: none; }
.qBox { background: var(--solid); border: 2px solid var(--line); padding: 18px 16px;
width: min(320px, 84vw); display: flex; flex-direction: column; gap: 10px; }
.qBox p { margin: 0 0 6px; font: 400 12px Silkscreen, monospace; color: var(--cream);
text-align: center; letter-spacing: 1px; }
@media (pointer: coarse) { #hint { display: none; } }
/* ---- THE MENU PALETTE. The canvas HUD keeps its own blue-grey and gold; the menus are
deliberately a different room — dark court green, one acid accent, and one crimson reserved for
@@ -355,6 +368,14 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
<canvas id="c" width="480" height="270"></canvas>
<div id="safe"></div>
<div id="hint">move: WASD/arrows &nbsp;·&nbsp; serve/swing: SPACE &nbsp;·&nbsp; aim: hold a direction while swinging &nbsp;·&nbsp; T: tuning panel</div>
<div id="quit" hidden><button id="qChip">&#10005;</button></div>
<div id="qAsk" hidden>
<div class="qBox">
<p>LEAVE THE MATCH?</p>
<button class="btn big" id="qYes">BACK TO THE MENU</button>
<button class="btn sec" id="qNo">PLAY ON</button>
</div>
</div>
<div id="intro" data-stage="splash">
<div class="card">
<!-- The title screen IS the menu. A separate menu stage behind it was the same logo over the
@@ -471,6 +492,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
</div>
</div>
</div>
<script type="module" src="src/main.js?v=237"></script>
<script type="module" src="src/main.js?v=238"></script>
</body>
</html>
+22
View File
@@ -230,6 +230,7 @@ function start(modeId = pickedMode) {
// differ by at this point: gore is the game as it always was, classic is the same tennis with the
// turn switched off, and zombie tennis is the match taking the court. Everything else about a mode
// lives in the code that reads G.classic / G.match, never here.
quitChip.hidden = false; // the door out rides every run (see the quit block below)
G.classic = modeId === 'classic';
// the difficulty rides on G like G.classic does, so a mid-run restart keeps the CPU you chose
G.diff = G.classic ? (DIFFS.find(d => d.id === pickedDiff) || DIFFS[1]) : null;
@@ -467,6 +468,7 @@ const maybeRestart = () => {
restartRun();
refreshSplash(); // the run just ended: the splash count has moved
intro.hidden = false;
quitChip.hidden = true; // the door out goes with the run it belonged to
// stats.lastRun() survives restartRun on purpose — it lives in the stats module, not on G, which
// has just been restored from PRISTINE. Consumed on read, so the prompt appears once.
const run = stats.lastRun();
@@ -476,6 +478,26 @@ const maybeRestart = () => {
cv.addEventListener('pointerdown', maybeRestart);
addEventListener('keydown', maybeRestart);
// THE DOOR OUT (Oliver: every tennis screen lacked an exit — force-closing the app was the only
// way back). The chip shows for the whole run; tapping it (or Escape on desktop) freezes the
// world and asks. Quitting is the death path minus the death: restartRun resets everything and
// nothing is banked — an abandoned run never happened, statistically.
const quitChip = document.getElementById('quit'), qAsk = document.getElementById('qAsk');
document.getElementById('qChip').addEventListener('click', () => { paused = true; qAsk.hidden = false; });
document.getElementById('qNo').addEventListener('click', () => { qAsk.hidden = true; paused = false; });
document.getElementById('qYes').addEventListener('click', () => {
qAsk.hidden = true; paused = false; quitChip.hidden = true;
restartRun();
refreshSplash();
intro.hidden = false;
go('splash');
});
addEventListener('keydown', e => {
if (e.key !== 'Escape' || !intro.hidden || quitChip.hidden) return;
qAsk.hidden = !qAsk.hidden;
paused = !qAsk.hidden;
});
// A tap on the SCOREBOARD toggles the miss-log overlay, and that is now all it does. The triple-tap
// that opened test mode has moved to the back room on the splash (see below): a cheat you can trip
// by accident mid-run is a bug, and this one sat on the one thing you tap to read the log.