Splatter feel pass: melee, homing, multi-ball, clearer MG mode

From Oliver's feedback round:

- Racket melee: any swish (tap) in splatter cleaves shamblers within
  TUNE.meleeRange (1.7m) — a close-range survival tool. Works during a
  horde too (tap = swish only, still no serve). The racket-zombie boss
  (Phase 6, not built) is immune by design.
- Zombies home in on you: on YOUR half (z > netZ) they now drift toward
  P.x instead of walking a straight lane; far half stays an aimless
  shamble.
- Multi-ball / rapid fire: splatter is now a shooter — every tap/flick
  fires immediately (dropped the toss ritual there), and serving while a
  ball still flies stashes the old one into a new extraBalls array so it
  keeps arcing and killing (cap 6). Refactored the kill into a shared
  killZombie() with no ball coupling, used by ball hits, pellets, melee,
  and extra balls.
- Machine-gun mode clarity: confirmed free-walk (drag/arrows move you,
  auto-assist off) and tap-doesn't-serve during a horde; added a
  code-drawn "rambo" racket held forward with a muzzle flash over the
  player, and a splatter-specific serve prompt.

REVIEW on your phone — two opinionated calls, both easy to revert:
1. Dropped the toss in splatter (tap = instant quick-serve, pow 0.62).
   Rapid fire, but no apex-timing quality/pierce. Want the toss back as
   an option?
2. MG posture is a code-drawn forward racket, not a real body sprite —
   good enough, or worth a proper p_mg sprite later?
Also: each rapid serve still auto-consumes a stocked bomb, so bombs
deplete fast now — flag if that feels wrong.

Verified via window.T: melee kills a 1.0m zombie and spares a 4.5m one;
a near-half zombie drifts x 3→1.9 toward you; firing twice leaves 1
stashed ball + a fresh serve (barrage screenshot); extra balls kill and
pierce; free-walk during a horde (x 0→2.7); horde-tap melees without
serving; tennis toss-serve and Phase-4 bomb consume still work; no
console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 4.8 committed 2026-07-11 22:54:31 +02:00
1 parent bf9553bd49
commit 9100bd5a23
5 files changed
+89 -28

No files matched your search

+20 -3
View File
@@ -4,7 +4,7 @@ import { cv, ctx } from './dom.js';
import { TUNE, COURT, PLAYER_H, BOY_H, UMP_H, BOUNCE_IDLE, SPRITE_H } from './config.js';
import { P, O, ball, score, crowd, G } from './state.js';
import { predictLanding } from './gameplay.js';
import { zombies, gore, piles, parts, pellets } from './zombies.js';
import { zombies, gore, piles, parts, pellets, extraBalls } from './zombies.js';
// ---------------------------------------------------------------- palette ----
// tennis palette -> drained splatter palette; the transition lerps between them
@@ -211,6 +211,17 @@ function drawFallbackChar(ch, p, h) { // shown only until sprites load
ctx.fillStyle = shorts; ctx.fillRect(x - 2.4 * u, y - 6 * u, 4.8 * u, 3 * u);
ctx.fillStyle = skin; ctx.fillRect(x - 1.8 * u, y - 3 * u, 1.4 * u, 3 * u); ctx.fillRect(x + 0.4 * u, y - 3 * u, 1.4 * u, 3 * u);
}
function drawMgRacket() { // horde: the player braces the racket forward and up-court, machine-gun style
const hip = proj(P.x + 0.15, 1.05, P.z);
const tip = proj(P.x, 0.95, P.z - 1.35); // pointing up-court, where the pellets stream out
ctx.strokeStyle = '#d8d8d8'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(hip.x, hip.y); ctx.lineTo(tip.x, tip.y); ctx.stroke();
ctx.strokeStyle = '#b07030'; ctx.lineWidth = 1.5; // the head, aimed forward
ctx.beginPath(); ctx.ellipse(tip.x, tip.y, Math.max(2, tip.s * 0.14), Math.max(3, tip.s * 0.22), 0, 0, 7); ctx.stroke();
if (G.horde && G.horde.mgT > 1 / TUNE.mgRate - 0.06) { // muzzle flash right after a shot
ctx.fillStyle = '#fff0a0'; ctx.beginPath(); ctx.arc(tip.x, tip.y, Math.max(2, tip.s * 0.12), 0, 7); ctx.fill();
}
}
// ------------------------------------------------------------------ frame ----
export function render() {
@@ -293,6 +304,8 @@ export function render() {
const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * BOUNCE_IDLE.apex;
drawBall(bx, by, sv.z);
}
// earlier serves still in flight (multi-ball barrage)
for (const b of extraBalls) drawBall(b.x, b.y, b.z, false, b.bomb);
// the match ball
if ((ball.active || G.phase === 'over') && !ball.collected) drawBall(ball.x, ball.y, ball.z, true, ball.bomb);
// splatter particles
@@ -308,6 +321,7 @@ export function render() {
ctx.fillStyle = '#f8e048'; ctx.fillRect((pp.x | 0) - 1, (pp.y | 0) - 1, 2, 2);
}
if (G.mode !== 'eaten') drawChar(P); // there is no P anymore
if (G.horde && G.mode === 'splatter') drawMgRacket(); // rambo racket over the player
// HUD — scoreboard
ctx.fillStyle = 'rgba(12,16,24,0.82)'; ctx.fillRect(6, 6, 132, 30);
@@ -336,8 +350,11 @@ export function render() {
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde) {
ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace';
const touch = matchMedia('(pointer: coarse)').matches;
ctx.fillText(G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
: (touch ? 'TAP TO TOSS, TAP TO SERVE' : 'SPACE TO TOSS, SPACE TO SERVE'), W / 2, H - 16);
const txt = G.mode === 'splatter'
? (touch ? 'TAP TO FIRE · FLICK TO AIM · SWISH TO MELEE' : 'SPACE FIRES · ARROWS AIM')
: G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
: (touch ? 'TAP TO TOSS, TAP TO SERVE' : 'SPACE TO TOSS, SPACE TO SERVE');
ctx.fillText(txt, W / 2, H - 16);
}
if (G.flash) {
ctx.font = 'bold 14px monospace'; ctx.fillStyle = '#ffffff';