Polish round: modal system, softened palette, pixel font, and the 14 plan items
The three foundations first, since everything else leans on them:
- G.modal is the only thing that stops the world. update() early-returns on it
so no zombie steps, no ball flight, no spawn timers run while a popup is on
screen — you can read it without being eaten. Dismissal swallows the gesture;
the pointerdown dismisser is registered only on pointer:fine, because on touch
pointerdown beats touchstart and a shared handler cleared the modal and then
served with the same thumb.
- UI in config.js is the accent palette. The arcade primaries were shouting over
the artwork; the gore reds deliberately stay loud.
- Every canvas font string now goes through F(). Bare `monospace` resolved to
Courier — a slab serif — which was the "generic serif" bug, not taste.
Then: weapons are only ever WON. deliverTin() has exactly one caller now
(resolveInterlude('P')); the old game schedule calls the DUEL instead, fired off
proceedAfterClear() where the court is guaranteed empty. Four popups (splatter
tutorial with a generated illustration, first duel, weapon received, horde),
wCap 9 with the tin amounts tunable, a 5-hp brute that comes apart over four
wound skins, fire that ignites the neighbours and chains outward, a three-stage
death where you become the biggest gore mound on the court, and a 7s changeover
with a ball boy who finally has a mop.
New art: assets/logo.png, x_howto, z_gw2-4, x_boymop1-4, plus assets/_key.py for
standalone art (chroma-key + crop, no frame splitting, no forced sprite height).
Benchmarks re-run and holding: CPU returns 23/24 serves, 30 human-tapped points
= 50 hits / 0 whiffs, 26 simulated games with no exceptions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5faaa99467
commit
a09a5d7559
23 files changed
+392
-103
No files matched your search
@@ -6,13 +6,14 @@ import { cv } from './dom.js';
|
||||
import { audio, pock } from './audio.js';
|
||||
import { TUNE } from './config.js';
|
||||
import { G, score } from './state.js';
|
||||
import { keyPressed, dragServe, selectWeapon, serveToss, pullQuality } from './gameplay.js';
|
||||
import { keyPressed, dragServe, selectWeapon, serveToss, pullQuality, dismissModal } from './gameplay.js';
|
||||
import { rackHit } from './render.js';
|
||||
import { togglePanel } from './panel.js';
|
||||
|
||||
const keys = {};
|
||||
addEventListener('keydown', e => {
|
||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) e.preventDefault();
|
||||
if (dismissModal()) return; // a modal eats the whole keystroke — dismissing must never also serve
|
||||
if (e.key === 't' || e.key === 'T') togglePanel();
|
||||
if (e.key >= '1' && e.key <= '5') selectWeapon(+e.key - 1); // pick a rack slot
|
||||
if (!keys[e.code]) keyPressed(e.code);
|
||||
@@ -45,7 +46,8 @@ export function takeFlickAim(maxAgeMs = 450) {
|
||||
// EVERY phase of splatter play, so the drag never silently turns into something else. The old
|
||||
// "drag walks the player" mode between shots was invisible state and read as a bug.
|
||||
const canAimServe = () =>
|
||||
G.mode === 'splatter' // every phase of it; 'eaten' is its own mode and never gets here
|
||||
!G.modal // the world is frozen behind a popup — the thumb that clears it must not draw a bow
|
||||
&& G.mode === 'splatter' // every phase of it; 'eaten' is its own mode and never gets here
|
||||
&& score.server === 'P' && G.weapon !== 4 // GUN pops on tap, no reticle
|
||||
&& !G.interlude && !G.horde && !(G.mgT > 0) && !G.changeover; // the duel serves like tennis: no reticle
|
||||
|
||||
@@ -56,6 +58,7 @@ cv.addEventListener('touchstart', e => {
|
||||
e.preventDefault(); audio();
|
||||
const r = cv.getBoundingClientRect();
|
||||
for (const t of e.changedTouches) {
|
||||
if (dismissModal()) return; // the popup owns this gesture and nothing else sees it
|
||||
if (!G.horde && !(G.mgT > 0)) { // rack tap selects a tin — but never during finger-steer modes
|
||||
const slot = rackHit((t.clientX - r.left) / r.width, (t.clientY - r.top) / r.height);
|
||||
if (slot >= 0) { selectWeapon(slot); continue; }
|
||||
@@ -121,3 +124,9 @@ const endTouch = e => {
|
||||
};
|
||||
cv.addEventListener('touchend', endTouch, { passive: false });
|
||||
cv.addEventListener('touchcancel', endTouch, { passive: false });
|
||||
|
||||
// Mouse/desktop dismissal. Registered ONLY on fine pointers: on a touch screen pointerdown
|
||||
// fires before touchstart, so a shared handler would clear the modal and let the touchstart
|
||||
// below it fall through into a serve — exactly the swallowed-gesture bug.
|
||||
if (!matchMedia('(pointer: coarse)').matches)
|
||||
cv.addEventListener('pointerdown', () => dismissModal());
|
||||
Reference in new issue
Block a user