Let the player choose a court before the match

Three courts, picked on a second start-screen stage: CENTRE COURT (grass),
TERRE ROUGE (clay) and THE SLAB (hard). A court is a row in config.COURTS and
four PNGs — render() never learns their names.

Each row carries two palettes, because every court still rots: what "drained"
means on orange clay is not what it means on green lawn. Mow stripes became a
flag rather than a colour — recolouring the lawn and leaving the bands on still
read as grass. Bounce is two numbers copied into TUNE on selection, so the three
bounce sites in gameplay.js and the T panel stay untouched: grass skids low and
fast, clay sits up and drags, hard is the old baseline.

Per-court crowd sheets and ad strips. Each court's clap frame is generated from
its OWN seated sheet, not from x_crowd2 — pairing one court's crowd with
another's applause cross-fades between two different crowds.

The picker is a second panel in the existing #intro overlay, rows built from the
table. Last pick is remembered and comes up pre-highlighted; 1..N or Enter on a
keyboard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-01 10:25:30 +02:00
1 parent 6378ff2a1b
commit fa30fb94c4
15 files changed
+166 -36

No files matched your search

+29 -6
View File
@@ -3,8 +3,8 @@
// Module map: config (tuning/constants) · state (mutable game state) · gameplay (logic/update)
// render (camera/drawing) · input (keys/touch) · audio (sfx/umpire) · panel (T tuning UI)
import { TUNE, COURT, UMP_H, UMP_X, EATEN_RESTART } from './config.js';
import { P, O, ball, score, G, setupPoint } from './state.js';
import { TUNE, COURT, COURTS, UMP_H, UMP_X, EATEN_RESTART } from './config.js';
import { P, O, ball, score, G, setupPoint, selectCourt } from './state.js';
import { update, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint, deliverTin, selectWeapon, showModal, dismissModal, startDuel, resolveInterlude, flickTarget, startBossFight, endBossFight } from './gameplay.js';
import { fitViewport, render, proj, RES } from './render.js';
import { voice, ambient } from './audio.js';
@@ -39,13 +39,28 @@ setupPoint();
// warm the pixel HUD font so the canvas never paints a system-serif fallback frame
if (document.fonts) { document.fonts.load('700 8px Silkscreen'); document.fonts.load('400 8px Silkscreen'); }
// intro overlay: first tap dismisses it, unlocks audio (iOS needs the gesture) and starts play
// intro overlay, two stages: the first tap opens court select, picking a court starts play and
// unlocks audio (iOS needs the gesture, and the SECOND tap is still a gesture — the pick is what
// has to unlock it, not the splash).
const intro = document.getElementById('intro');
function dismissIntro() {
const courts = document.getElementById('courts');
const lastCourt = (() => { try { return localStorage.getItem('court'); } catch { return null; } })();
for (const c of COURTS) {
const b = document.createElement('button');
b.className = c.id === (lastCourt || COURTS[0].id) ? 'on' : ''; // your last court comes up pre-picked
b.innerHTML = `<img src="assets/${c.tile}.png?v=81" alt=""><span><b>${c.name}</b><i>${c.sub}</i></span>`;
b.addEventListener('pointerdown', e => { e.preventDefault(); e.stopPropagation(); start(c.id); });
courts.appendChild(b);
}
function openPicker() { if (intro.parentNode) intro.classList.add('picking'); }
function start(id) {
if (!intro.parentNode) return;
selectCourt(id);
intro.remove(); ambient(); voice('quiet_please'); // low atmosphere bed starts with the first gesture
}
intro.addEventListener('pointerdown', e => { e.preventDefault(); dismissIntro(); });
// a tap anywhere on the splash opens the picker; on the picker itself only the buttons act, so a
// stray tap beside a tile can't start a match on a court you didn't choose.
intro.addEventListener('pointerdown', e => { e.preventDefault(); openPicker(); });
// which build am I looking at? Push = deploy and the browser cannot see git, so the served file's
// own timestamp IS the version marker — it changes on every deploy, with nothing to remember to bump.
// Match it against `git log -1 --format=%cd` when a phone looks suspiciously out of date.
@@ -53,7 +68,15 @@ fetch('main.js', { method: 'HEAD' })
.then(r => { const t = r.headers.get('last-modified'); if (!t) return;
document.getElementById('build').textContent = 'BUILD ' + new Date(t).toLocaleString('en-GB').replace(',', ''); })
.catch(() => {}); // no header, no line — never a broken start screen
addEventListener('keydown', () => dismissIntro(), { once: true });
// keyboard: any key opens the picker, then 1..N picks a court and Enter/Space takes the
// highlighted one. Not `once` any more — it has two stages to get through.
addEventListener('keydown', e => {
if (!intro.parentNode) return;
if (!intro.classList.contains('picking')) { openPicker(); return; }
const n = parseInt(e.key, 10);
if (n >= 1 && n <= COURTS.length) start(COURTS[n - 1].id);
else if (e.key === 'Enter' || e.key === ' ') start(lastCourt || COURTS[0].id);
});
// eaten: any tap or key restarts — but only once the death animation has played out, so a
// panic tap during the gore burst can't skip it. A modal owns its own gesture (see input.js).