Splatter: the pull IS the shot — direction aims, length is power
The ball economy made it harder in the wrong way: waiting for a ballboy is friction, not difficulty, and walking with the finger was one gesture too many for anyone to discover. Both are out — a spent ball hands you a fresh one again, and the thumb only ever does the slingshot. The brake moves into the gesture instead. The pull's LENGTH is now the shot's power and nothing else: under TUNE.zSplatQ it merely staggers, at full draw it's quality 1.0 with the PERFECT flash and hitstop. A lazy rhythm of short pulls can't kill anything, so tempo stops being a strategy. The pull's DIRECTION is a ray, not a landing point, which is the ergonomic fix for having to drag off the bottom of the phone: distance to the target no longer costs drag length. The first shambler standing in the lane eats the ball, whether it's 5 or 21 metres away, and the cone widens with range so distant lanes stay pickable. Full power now sits at 95px of travel. Also gone: the free predictive auto-lock on a directionless aim (zombieTarget returns null with no direction, and a bare hold no longer fires at all). Melee goes back to cleaving the cluster — it's the panic button — with the 1s cooldown left in as the brake. The reticle reads power throughout: cool blue only staggers, hot red kills, the drawn band grows behind you, and the pull clicks the moment it crosses the kill threshold. Verified by simulated touch gestures: 10px cancels, 35px = 0.31 (stagger), 60px = 0.60, 95px = 1.00; the same 60px pull reaches shamblers at 5m, 21m and diagonally; 22 serves in 30s, so the pace is back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2787778ad2
commit
ff973cf6bb
6 files changed
+82
-97
No files matched your search
+12
-14
@@ -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, giveBall } from './gameplay.js';
|
||||
import { splatPoint, cheer, spentBall, resolveInterlude } 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,20 +80,18 @@ function woundBurst(zb) {
|
||||
t: 0.35 + Math.random() * 0.3, dark: Math.random() < 0.6 });
|
||||
}
|
||||
|
||||
// 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)
|
||||
// swishing the racket cleaves every shambler in reach — it's the panic button for when they're
|
||||
// already on top of you, so it stays a full sweep. TUNE.meleeCool is the brake that stops it
|
||||
// from being the default attack. (the racket-zombie boss shrugs melee off entirely)
|
||||
export function meleeSwipe(x, z, range) {
|
||||
let best = -1, bd = range;
|
||||
for (let i = 0; i < zombies.length; i++) {
|
||||
let hit = 0;
|
||||
for (let i = zombies.length - 1; i >= 0; i--) {
|
||||
const zb = zombies[i];
|
||||
if (zb.variant === 'r') continue;
|
||||
const d = Math.hypot(zb.x - x, zb.z - z);
|
||||
if (d < bd) { bd = d; best = 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 (best < 0) return 0;
|
||||
killZombie(best, !!G.horde); swish(0.35);
|
||||
return 1;
|
||||
if (hit) swish(0.35);
|
||||
return hit;
|
||||
}
|
||||
|
||||
function splatter(i, fanfare = true) { // the MAIN ball hit a zombie
|
||||
@@ -123,7 +121,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
|
||||
giveBall(); setupPoint(); // the wave is over: a ball in hand, and any zombies still standing are yours to mop up
|
||||
setupPoint(); // serve loop resumes; any zombies still standing are yours to mop up
|
||||
voice('quiet_dark', 0.5, 0.85);
|
||||
}
|
||||
function tickHorde(dt) {
|
||||
@@ -184,7 +182,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) { giveBall(); setupPoint(); } // ammo dry — the serve loop resumes, ball in hand
|
||||
if (G.mgT <= 0) setupPoint(); // ammo dry — the serve loop resumes
|
||||
}
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user