Kills now grow 3D-ish gore mounds, not just flat stains. zombies.js gains
a piles array {x,z,size,seed}: a kill within 0.6m of a pile grows it
(size +1, cap 4), else starts a new one (cap 30, oldest shifts out).
Ball boys shave overlapping piles at size -= dt*0.25 — far slower than
stain mopping (dt*1.6) — so net growth wins over a long match. Splatter
particles bumped 14→24, ~1/3 dark and longer-lived (settling gore),
render reads pt.dark for colour.
render.js draws each pile as seeded pixel-art blobs (mulberry32 from
p.seed): N=6+size*5 blobs, offset within size*0.28m, lifted up to
size*0.14m so the pile has visible height, weighted gore colours + ~10%
bone flecks. Blobs are built once and rebuilt only when the pile GROWS
(builtSize guard) — never on the per-frame mop-shrink — and each pile
costs a single proj() call (blob screen positions offset from the base).
Piles draw split far/near of the net like zombies so height occludes
right. window.T exposes render/piles/gore/parts for verification.
Verified via window.T: clustered kills build tall dense mounds while
singles stay small (screenshot), avg render 0.61ms with 10 piles, 0 blob
rebuilds across 120 frames, mop shrinks a size-4 pile at exactly 0.25/s,
no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
2.7 KiB
JavaScript
60 lines
2.7 KiB
JavaScript
// Splatter Tennis — boot, main loop, intro overlay, and the window.T test hooks.
|
|
// World units: meters. z axis = court depth, 0 at FAR baseline, 23.77 at NEAR baseline.
|
|
// 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 } from './config.js';
|
|
import { P, O, ball, score, G, setupPoint } from './state.js';
|
|
import { update, keyPressed, solveShot, inServeBox, inCourt, forceZombies } from './gameplay.js';
|
|
import { fitViewport, render, proj } from './render.js';
|
|
import { voice } from './audio.js';
|
|
import { zombies, spawnZombie, piles, gore, parts } from './zombies.js';
|
|
import { cv } from './dom.js';
|
|
import './input.js';
|
|
|
|
let last = performance.now();
|
|
function frame(now) {
|
|
const dt = Math.min(0.033, (now - last) / 1000); last = now;
|
|
update(dt); render();
|
|
requestAnimationFrame(frame);
|
|
}
|
|
|
|
fitViewport();
|
|
setupPoint();
|
|
|
|
// intro overlay: first tap dismisses it, unlocks audio (iOS needs the gesture) and starts play
|
|
const intro = document.getElementById('intro');
|
|
function dismissIntro() {
|
|
if (!intro.parentNode) return;
|
|
intro.remove(); voice('quiet_please');
|
|
}
|
|
intro.addEventListener('pointerdown', e => { e.preventDefault(); dismissIntro(); });
|
|
addEventListener('keydown', () => dismissIntro(), { once: true });
|
|
|
|
// eaten: any tap or key (after a beat, so death-spam doesn't skip the screen) restarts
|
|
const maybeRestart = () => { if (G.mode === 'eaten' && G.eatenT > 0.7) location.reload(); }; // ponytail: reload = honest full reset
|
|
cv.addEventListener('pointerdown', maybeRestart);
|
|
addEventListener('keydown', maybeRestart);
|
|
|
|
// hidden shortcut: tap the umpire three times to summon zombie mode early
|
|
let umpTaps = [];
|
|
cv.addEventListener('pointerdown', e => {
|
|
const r = cv.getBoundingClientRect();
|
|
const x = (e.clientX - r.left) / r.width * (cv.width / 2); // logical coords (RES = 2)
|
|
const y = (e.clientY - r.top) / r.height * (cv.height / 2);
|
|
if (x >= 6 && x <= 138 && y >= 6 && y <= 36) { G.logShow = !G.logShow; return; } // scoreboard tap = miss-log overlay
|
|
const u = proj(7.1, 0, COURT.netZ);
|
|
if (Math.abs(x - u.x) > 16 || y > u.y + 6 || y < u.y - UMP_H * u.s - 8) return;
|
|
const now = performance.now();
|
|
umpTaps = umpTaps.filter(t => now - t < 1600);
|
|
umpTaps.push(now);
|
|
if (umpTaps.length >= 3) { umpTaps = []; forceZombies(); }
|
|
});
|
|
|
|
requestAnimationFrame(frame);
|
|
|
|
// exposed for in-browser sanity checks and simulated playtests
|
|
window.T = { TUNE, ball, P, O, score, G, zombies, spawnZombie, piles, gore, parts, solveShot, inServeBox, inCourt, proj, setupPoint,
|
|
step: dt => update(dt), render, press: keyPressed,
|
|
get phase() { return G.phase; }, set phase(v) { G.phase = v; } };
|