Clear the court of everything that is neither the puzzle nor the threat

"remove the mob boy - make the normal zombies rarer and not so close to the
color gang".

sendBoy refuses during a drill, guarded at the function rather than at the
call sites — same shape as its existing G.interlude guard, so every present
and future caller inherits it. He is one more body crossing the lanes you are
reading colours down, and unlike everything else out there he is neither the
puzzle nor the threat. Measured: 0 boy-frames in 90 seconds. Gore no longer
gets mopped in this mode; the existing 40/30 caps are what bound it.

The eaters are rarer — every 12s falling to 5s, capped at 3 on court. At the
old rate they were a second crowd rather than an interruption, and a threat
you break off to deal with only works if breaking off is occasional. 8
arrivals in 90s where the old numbers gave ~13.

And they enter wide of the colour gang: best-of-9 lane sampling against the
bibbed bodies, the same trick the batch uses. Narrowest clearance measured
across 8 spawns was 0.91 m. They still converge on you afterwards — that is
what they are for — this only decides where they walk in, so they stop
screening the bodies you are trying to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 20:08:58 +02:00
1 parent d775ac40ec
commit 3379e83efc
5 files changed
+45 -8

No files matched your search

+8 -3
View File
@@ -360,9 +360,14 @@ export const TUNE = {
drillSpeed: 0.80, // m/s a bibbed body walks at the bottom of the ramp...
drillSpeedMax: 1.9,// ...and at the top. Stamped per body AT SPAWN, so the ramp shows up as new
// arrivals outpacing the ones already walking, not the court accelerating
drillEaterEvery: 7.0, // s between unbibbed eaters at the bottom of the ramp...
drillEaterMin: 2.2, // ...and at the top. THE difficulty dial now that the puzzle plays fair
drillEaterMax: 4, // eaters allowed on court at once — past this the puzzle is unreadable
// The eaters are RARE on purpose. At every 7 s / 2.2 s they were a second crowd rather than an
// interruption, and the puzzle is the point of the mode — a threat you have to break off and deal
// with only works if breaking off is occasional. They also enter wide of the colour gang
// (spawnEater), because the one body out there that is neither the puzzle nor worth anything has
// no business standing in front of the puzzle.
drillEaterEvery: 12.0, // s between unbibbed eaters at the bottom of the ramp...
drillEaterMin: 5.0, // ...and at the top. Still THE difficulty dial, just a slower one
drillEaterMax: 3, // eaters allowed on court at once — past this the puzzle is unreadable
drillEaterSpeed: 1.15,// they walk a touch faster than the batch: the batch is a list, this is a threat
drillRampT: 210, // s to go from the bottom of every curve above to the top
drillRingR: 0.62, // m — radius of the colour disc a bibbed body walks on. Big enough to read at
+13 -2
View File
@@ -101,8 +101,19 @@ function spawnBatch(d) {
// mode was missing.
function spawnEater(d) {
if (zombies.filter(z => z.bib === undefined).length >= TUNE.drillEaterMax) return;
spawnZombie((Math.random() * 2 - 1) * TUNE.drillSpread, TUNE.drillSpawnZ + Math.random() * 0.8,
Math.random() < 0.3 ? 'b' : 'a');
// THEY ENTER WIDE OF THE COLOUR GANG. An eater walking in on top of the batch screens the very
// bodies you are trying to read, and it is the one body on court that is neither the puzzle nor
// worth points — so it has no business standing in front of the puzzle. Best-of-N: sample lanes
// across the width and take the one furthest from every coloured body. They still converge on you
// afterwards (that is what they are for); this only decides where they come IN.
const gang = bibs();
let bx = 0, bs = -1;
for (let k = 0; k < 9; k++) {
const x = -TUNE.drillSpread + (2 * TUNE.drillSpread) * (k / 8) + (Math.random() - 0.5) * 0.4;
const s = gang.length ? Math.min(...gang.map(z => Math.abs(z.x - x))) : 1e9;
if (s > bs) { bs = s; bx = x; }
}
spawnZombie(bx, TUNE.drillSpawnZ + Math.random() * 0.8, Math.random() < 0.3 ? 'b' : 'a');
const zb = zombies[zombies.length - 1];
zb.v = drillSpeed(d) * TUNE.drillEaterSpeed * (0.9 + Math.random() * 0.2);
}
+6 -2
View File
@@ -104,8 +104,9 @@ export const G = {
interlude: false, // sudden-death duel vs the racket zombie: falsy, or {introT} while announcing
duelLerp: 0, // 0..1 — how far the court has returned to tennis colors for the duel
horde: null, // active horde wave {t, spawnT, mgT} — machine-gun racket, no serving
drill: null, // the coaching drill (src/drill.js): {seq, step, faults, callT, beat, rush, won, lost, msg}.
// Owns the court like every other mode — see beats.startDrill.
drill: null, // the coaching drill (src/drill.js): {seq, step, faults, t, eaterT, rounds, lost, msg}.
// Owns the court like every other mode — see beats.startDrill. `t` is time
// survived and it is the whole difficulty curve; there is no round clock.
killCount: 0, // splatter: kills banked toward the next point (see TUNE.killsPerPoint)
runGames: 0, // games finished this run (either side) — the difficulty director
sets: [0, 0], // splatter sets won [you, them]; 6 games = a set = a horde
@@ -181,6 +182,9 @@ const mopWorthIt = () => gore.some(d => d.a >= 0.15 && d.z >= COURT.netZ - 2);
export function sendBoy(tx = ball.x, tz = ball.z, mop = G.mode === 'splatter') {
if (G.interlude) return; // the sudden-death duel is clean tennis: nobody mops through it
if (G.drill) return; // ...and nobody walks onto a drill. He is one more body crossing the
// lanes you are reading colours down, and unlike everything else out
// there he is not part of the puzzle OR the threat — pure noise.
if (mop && !mopWorthIt()) return; // no job, no boy
if (tz < -3 || tz > 27) return; // target left the field lengthwise, let it go
// ONE boy, one slot. Every kill used to call this and teleport him back to the wing mid-stride,