Files
splatter-tennis/main.js
T
wuwiandClaude Opus 4.8 eacad1ffca Sound: recorded SFX layer + a touch-easier CPU
Wire the new assets/audio/*.wav samples over the synth SFX:
- Ball hits: per-side tennis-player / tennis-opponent on serve & return,
  now with a stadium reverb tail (same convolver as umpire/aww).
- Crowd: tennis-applaus on your points/games (louder on big moments);
  tennis-aww when the opponent scores or you're eaten (reverb tail).
- Ambient bed: tennis-ambient-loop from first tap, 500Hz lowcut, low vol.
- Zombie music: zombie-mode loop when splatter kicks in (800Hz lowcut),
  fades out for the 1:1 sudden-death duel and back in after.
- Headshots: random headshot-1/-2 on every zombie kill (killZombie).

audio.js: sfx() one-shot player w/ rate+reverb opts, loopBed() beds with
duck/fade, aww() wrapper, hotter reverb send on the umpire voice.

Also: normal opponent aiFlub 0.09 -> 0.14 (a little easier to beat).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 20:33:29 +02:00

61 lines
3.1 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, UMP_X } from './config.js';
import { P, O, ball, score, G, setupPoint } from './state.js';
import { update, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint } from './gameplay.js';
import { fitViewport, render, proj, RES } from './render.js';
import { voice, ambient } from './audio.js';
import { zombies, spawnZombie, piles, gore, parts, pellets, extras, explode } from './zombies.js';
import { cv } from './dom.js';
import './input.js';
let last = performance.now();
function frame(now) {
requestAnimationFrame(frame); // schedule FIRST: one bad frame must never kill the loop (a thrown error froze the game for good)
if (TUNE.fps && now - last < 1000 / TUNE.fps - 2) return; // frame cap (battery): skip RAF ticks until it's our beat
const dt = Math.min(0.04, (now - last) / 1000); last = now;
update(dt); render();
}
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(); ambient(); voice('quiet_please'); // low atmosphere bed starts with the first gesture
}
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 / RES); // logical coords
const y = (e.clientY - r.top) / r.height * (cv.height / RES);
if (x >= 6 && x <= 138 && y >= 6 && y <= 36) { G.logShow = !G.logShow; return; } // scoreboard tap = miss-log overlay
const u = proj(UMP_X, 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, pellets, extras, explode, splatPoint, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies,
step: dt => update(dt), render, press: keyPressed,
get phase() { return G.phase; }, set phase(v) { G.phase = v; } };