Splatter: ball economy — one ball, fetch it back, no more toss-spam

The slingshot serve didn't fix the gameplay because four things were free at
once: pure-hold auto-target with predictive lead, a hardcoded 0.62 serve
quality, infinite ammo with an instant mid-rally re-toss, and a melee swish on
every single tap that cleaved the whole cluster. Perfecting the pretoss/toss
rhythm beat the game; no skill required.

The ball is scarce now. A spent ball LIES where it died (G.loose, ringed on
court) and there is no serve until it's back in your hand: walk it down
(instant, and the only reason to leave the baseline), let a boy scoop it, wait
out TUNE.looseMax, or watch a shambler swallow it — the one real punishment.
Fire rate is a positioning decision instead of a thumb-tempo one.

Also: the far backstop rebounds a ball with pace left, still lethal on the way
home and volleyable (trySwing lets P re-hit their own ball in splatter); melee
drops to nearest-one-only on a 1s cooldown; and the mid-rally re-toss is gone.
With no ball in hand the thumb-drag steers your legs instead of the reticle —
without that, walking your ball down is impossible on touch.

TUNE.looseMax is the fire-rate knob. Measured: ~4s per serve standing still,
~0.4s if you go and get it. 600 taps in 10s now yield exactly one serve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-07-25 14:18:07 +03:00
1 parent 013fe8152d
commit 2787778ad2
6 files changed
+95 -35

No files matched your search

+14 -10
View File
@@ -6,7 +6,7 @@
import { TUNE, COURT } from './config.js';
import { G, P, ball, sendBoy, setupPoint } from './state.js';
import { splat, moan, pock, voice, boom, swish, shriek, aww, sfx } from './audio.js';
import { splatPoint, cheer, spentBall, resolveInterlude } from './gameplay.js';
import { splatPoint, cheer, spentBall, resolveInterlude, giveBall } from './gameplay.js';
export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'}
export const gore = []; // lawn decals {x, z, r, a} — the flat ground layer
@@ -80,16 +80,20 @@ function woundBurst(zb) {
t: 0.35 + Math.random() * 0.3, dark: Math.random() < 0.6 });
}
// swishing the racket cleaves any shambler in reach (the racket-zombie boss shrugs melee off)
// swishing the racket takes out the NEAREST shambler in reach — one swing, one body. It used to
// cleave the whole cluster, which made standing still and tapping better than playing tennis.
// (the racket-zombie boss shrugs melee off entirely)
export function meleeSwipe(x, z, range) {
let hit = 0;
for (let i = zombies.length - 1; i >= 0; i--) {
let best = -1, bd = range;
for (let i = 0; i < zombies.length; i++) {
const zb = zombies[i];
if (!zb || zb.variant === 'r') continue; // !zb: killZombie can sweep the court mid-loop
if (Math.hypot(zb.x - x, zb.z - z) < range) { killZombie(i, !!G.horde); hit++; }
if (zb.variant === 'r') continue;
const d = Math.hypot(zb.x - x, zb.z - z);
if (d < bd) { bd = d; best = i; }
}
if (hit) swish(0.35);
return hit;
if (best < 0) return 0;
killZombie(best, !!G.horde); swish(0.35);
return 1;
}
function splatter(i, fanfare = true) { // the MAIN ball hit a zombie
@@ -119,7 +123,7 @@ export function spawnRacketZombie() { // one takes up residence on the far court
function endHorde() {
G.horde = null; pellets.length = 0;
if (!zombies.some(z => z.variant === 'r')) spawnRacketZombie(); // survive the wave → a racket zombie stays
setupPoint(); // serve loop resumes; any zombies still standing are yours to mop up
giveBall(); setupPoint(); // the wave is over: a ball in hand, and any zombies still standing are yours to mop up
voice('quiet_dark', 0.5, 0.85);
}
function tickHorde(dt) {
@@ -180,7 +184,7 @@ export function updateZombies(dt) {
else if (G.mgT > 0 && G.phase === 'rally') { // machine-gun tin: horde firepower, regular rules
G.mgT -= dt;
if ((G.mgCool -= dt) <= 0) { firePellet(); G.mgCool = 1 / TUNE.mgRate; }
if (G.mgT <= 0) setupPoint(); // ammo dry — the serve loop resumes
if (G.mgT <= 0) { giveBall(); setupPoint(); } // ammo dry — the serve loop resumes, ball in hand
}
if (!G.horde && !G.changeover && !G.interlude && !G.clearing) { // breathers, duels AND the mop-up after a won game get a quiet court (no new spawns)
spawnT -= dt;