diff --git a/config.js b/config.js index cf3c775..08a9f22 100644 --- a/config.js +++ b/config.js @@ -14,7 +14,7 @@ export const TUNE = { serveSpeed: 21, serveQualitySpeed: 7, aimError: 2.4, // m of scatter at quality 0 perfectMs: 45, // |offset from window center| for PERFECT - aiQualityMin: 0.45, aiQualityMax: 0.85, aiFlub: 0.14, // ~86% of returns land (was 0.09) — a touch easier to beat + aiQualityMin: 0.45, aiQualityMax: 0.85, aiFlub: 0.09, aiReaction: 0.45, // s before the CPU reads your shot — well-aimed corners are winners autoAssist: matchMedia('(pointer: coarse)').matches ? 1.15 : 0, // auto-run speed factor (mobile) zSpeed: 0.7, // zombie shamble speed m/s diff --git a/zombies.js b/zombies.js index 6f8f60c..efb5612 100644 --- a/zombies.js +++ b/zombies.js @@ -19,7 +19,8 @@ let spawnT = 1.5, moanT = 3; export function spawnZombie(x, z, variant) { const ramp = 1 + Math.min(1.5, G.splatT / 70); // they get faster the longer you survive - const zb = { x, z, v: TUNE.zSpeed * ramp * (0.8 + Math.random() * 0.4), walkT: Math.random() * 9, variant }; + const zb = { x, z, v: TUNE.zSpeed * ramp * (0.8 + Math.random() * 0.4), walkT: Math.random() * 9, + homeOff: (Math.random() * 2 - 1) * 0.8, variant }; // personal lane offset so they don't stack into a column if (variant === 'f') { zb.v = Math.min(TUNE.runnerVMax, zb.v * TUNE.runnerSpeed); shriek(); } // shriek = the fairness contract; capped so late runners stay reactable if (variant === 'g') { zb.v *= 0.55; zb.hp = TUNE.bruteHp; } // slow, but soaks hits if (variant === 's') zb.shieldT = 0; // chair up; > 0 = seconds of stagger left @@ -184,11 +185,13 @@ export function updateZombies(dt) { zb.z += zb.v * dt; if (zb.shieldT > 0) zb.shieldT -= dt; // staggered chair comes back up when this runs out if (zb.variant === 'r') { /* the racket boss holds station — its AI owns its x */ } - else if (G.mode === 'splatter' && zb.z > COURT.netZ) // on YOUR half they home in on you - zb.x += Math.sign(P.x - zb.x) * Math.min(Math.abs(P.x - zb.x), zb.v * 0.7 * dt); - else + else if (G.mode === 'splatter' && zb.z > COURT.netZ) { // on YOUR half they close in — each on its own lane, not a column + const tx = P.x + zb.homeOff; + zb.x += Math.sign(tx - zb.x) * Math.min(Math.abs(tx - zb.x), zb.v * 0.7 * dt) + + Math.sin(zb.walkT * 2.3) * 0.25 * dt; // shamble wobble — the approach reads organic, not tracked + } else zb.x += Math.sin(zb.walkT * 1.7) * 0.15 * dt; // far half: aimless shamble - if (G.mode === 'splatter' && Math.hypot(zb.x - P.x, zb.z - P.z) < TUNE.eatenR + (zb.variant === 'g' ? 0.2 : 0)) { // it got you — touch is HONEST now: sprite overlap = eaten + if (G.mode === 'splatter' && Math.hypot(zb.x - P.x, (zb.z - P.z) * 0.75) < TUNE.eatenR + (zb.variant === 'g' ? 0.2 : 0)) { // z weighted down: depth is foreshortened, so a head-on shambler eats you when it LOOKS on top of you gore.push({ x: P.x, z: P.z, r: 0.9, a: 0.9 }); for (let k = 0; k < 30; k++) parts.push({ x: P.x, y: 1.1, z: P.z,