WIP splatter feedback round + HANDOFF.md for session continuation

Done (unverified): faster serve cadence in splatter (overT 0.25, serve from
where you stand, flick = instant aimed quick serve), aim-anywhere targeting
relative to the player. Not done: zombie speed ramp (G.splatT exists but is
never incremented), eaten-on-touch death + restart screen. Full state and
plans in HANDOFF.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-07-11 19:28:00 +02:00
1 parent 60c5f72a0a
commit e7aab295d4
4 files changed
+140 -15

No files matched your search

+25 -11
View File
@@ -25,13 +25,16 @@ export function predictLanding() { // first-bounce point, closed form — exact,
// the best-matching zombie anywhere on the court — including your own side of the net.
function zombieTarget(aim) {
if (!zombies.length) return null;
const lane = (aim ? aim.ax : 0) * 3.3;
const band = aim && aim.az !== 0 ? (aim.az < 0 ? 4 : 19) : null;
const none = !aim || (!aim.ax && !aim.az);
let best = null, bs = 1e9;
for (const zb of zombies) {
let s = Math.abs(zb.x - lane);
if (band !== null) s += Math.abs(zb.z - band) * 0.5;
if (!aim) s = COURT.L - zb.z; // no aim (serves): the nearest one is the most urgent
let s;
if (none) s = COURT.L - zb.z; // no direction: the most advanced one is the most urgent
else {
s = Math.abs(zb.x - (P.x + aim.ax * 4)); // lane relative to where YOU stand
if (aim.az < 0) s += zb.z * 0.35; // flick up: prefer the far ones
if (aim.az > 0) s += (COURT.L - zb.z) * 0.35; // flick down: the close ones on your side
}
if (s < bs) { bs = s; best = zb; }
}
return best;
@@ -93,11 +96,11 @@ function tossBall(sv) {
}
export function serveApexY() { return TOSS.y0 + TOSS.vy * TOSS.vy / (2 * TUNE.gravity); } // ~2.95
function hitServe(sv, quality) {
function hitServe(sv, quality, aim = null) {
const toFar = sv.side === 'P';
const s = TUNE.serveSpeed + quality * TUNE.serveQualitySpeed;
let boxX, tzs;
const zt = G.mode !== 'tennis' ? zombieTarget(null) : null;
const zt = G.mode !== 'tennis' ? zombieTarget(aim) : null;
if (zt) { // splatter serves auto-track the most advanced zombie
boxX = zt.x + (Math.random() * 2 - 1) * (1 - quality) * 0.8;
const T0 = Math.hypot(zt.x - ball.x, zt.z - ball.z) / s;
@@ -290,7 +293,18 @@ function trySwing(ch, isAI) {
export function keyPressed(code) {
if (code !== 'Space') return;
audio(); // unlock on first gesture
if (G.phase === 'pretoss' && score.server === 'P') { tossBall(P); return; }
if (G.phase === 'pretoss' && score.server === 'P') {
if (G.mode === 'splatter') {
const flick = takeFlickAim();
if (flick) { // quick serve: no toss ritual, modest pace, aimed by the flick
ball.active = true; ball.collected = false; ball.bounces = 0; ball.firstLand = null;
ball.x = P.x + 0.35; ball.z = P.z; ball.y = 2.2; ball.g = TUNE.gravity;
hitServe(P, 0.62, flick);
return;
}
}
tossBall(P); return;
}
if (G.phase === 'toss' && score.server === 'P') {
// serve timing: distance from apex = quality
const dy = Math.abs(ball.y - serveApexY());
@@ -412,9 +426,9 @@ export function update(dt) {
onBounce();
if (ball.active && Math.abs(ball.vy) < 0.8) { // rolled out — dead ball
ball.active = false;
if (G.mode === 'splatter') { // hand the server a fresh one
G.phase = 'over'; G.overT = 0.7; sendBoy();
if (Math.random() < 0.2) voice('new_balls', 0.3);
if (G.mode === 'splatter') { // hand the server a fresh one, fast — they're coming
G.phase = 'over'; G.overT = 0.25; sendBoy();
if (Math.random() < 0.12) voice('new_balls', 0.3);
} else if (ball.bounces === 1) pointTo(ball.lastHit, null);
}
}