Splatter feel pass: baseline turret, tennis duel, rapid serves, bleeding wounds

- Stand at the baseline: in splatter the player is pinned to the baseline
  (a turret, side-to-side only) — the duel is exempt so it keeps free depth.
- Zombie 1:1 duel now plays like real tennis: no free-aim flick, no drag
  reticle; lane aim + the toss-then-hit serve (gate on !G.interlude in
  doHit/hitServe/keyPressed/canAimServe).
- Rapid serves: the two-stage reticle serve now fires mid-rally too — a ball
  still in flight is banished to a ballistic clone so the next serve launches
  at once. Serve as fast as you can drag-release.
- Wounded skins for multi-hit foes: brute (arm off) and racket boss (arm off,
  then headless) swap by remaining hp, with a graceful fallback to the healthy
  sprite. The headless boss is drawn a head shorter so the shoulders line up.
- Dynamic gore: the blood is NOT painted in — open wounds gush live particles
  every frame (arm stumps + a neck geyser on the headless boss), absurdly, and
  bounded so it self-regulates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 4.8 committed 2026-07-17 20:39:07 +02:00
1 parent 7936d83f0f
commit df4eaf6e39
10 files changed
+53 -21

No files matched your search

+19 -15
View File
@@ -66,7 +66,7 @@ function doHit(ch, quality, aim) {
const err = () => (Math.random() * 2 - 1) * TUNE.aimError * (1 - quality);
let tx, tz;
let s = TUNE.baseShotSpeed + quality * TUNE.qualityShotSpeed;
const ft = G.mode !== 'tennis' && ch.side === 'P'
const ft = G.mode !== 'tennis' && !G.interlude && ch.side === 'P' // the duel aims like tennis: lanes, not free-aim
? (aim && aim.fx !== undefined ? flickTarget(ch, aim) : { zt: zombieTarget(aim) }) : null;
if (ft && ft.zt) {
const zt = ft.zt;
@@ -124,7 +124,7 @@ function hitServe(sv, quality, aim = null) {
const toFar = sv.side === 'P';
const s = TUNE.serveSpeed + quality * TUNE.serveQualitySpeed;
let boxX, tzs;
const ft = G.mode !== 'tennis'
const ft = G.mode !== 'tennis' && !G.interlude // the duel serves like tennis: to the box, not free-aim
? (aim && aim.fx !== undefined ? flickTarget(sv, aim) : { zt: zombieTarget(aim) }) : null;
if (ft && ft.zt) { // magnet hit, or (tap) the most advanced zombie
const zt = ft.zt;
@@ -547,9 +547,12 @@ function tryGunBurst() {
// release of the two-stage aim: an instant serve to the reticle (quality fixed — placement
// is this serve's power; the toss ritual keeps the PERFECT-timing power ceiling)
export function dragServe(aim) {
if (G.mode !== 'splatter' || G.phase !== 'pretoss' || score.server !== 'P') return;
if (G.horde || G.mgT > 0 || G.changeover || (G.interlude && G.interlude.introT > 0)) return;
if (G.mode !== 'splatter' || score.server !== 'P') return;
if (G.interlude || G.horde || G.mgT > 0 || G.changeover) return; // the duel serves like tennis, not the reticle
if (G.phase !== 'pretoss' && G.phase !== 'rally') return; // rally too: fire again while a ball's still up
if (tryGunBurst()) return;
if (ball.active) // rapid fire: the ball in flight keeps going as a ballistic clone so the next serve can launch now
extras.push({ x: ball.x, y: ball.y, z: ball.z, vx: ball.vx, vy: ball.vy, vz: ball.vz, pow: ball.pow || 0.5, t: 2.5 });
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, aim);
@@ -564,18 +567,16 @@ export function keyPressed(code) {
if (G.interlude && G.interlude.introT > 0) return; // let the umpire finish announcing the duel
// splatter: a tap/flick during the brief 'over' beat re-arms the next serve immediately
if (G.mode === 'splatter' && G.phase === 'over') { ball.active = false; setupPoint(); }
// splatter rapid serve: a flick fires an aimed quick serve — in pretoss OR mid-rally (dragServe
// banishes any live ball so the next launches now). The duel is exempt: it serves like tennis.
if (G.mode === 'splatter' && !G.interlude && score.server === 'P'
&& (G.phase === 'pretoss' || G.phase === 'rally')) {
const flick = takeFlickAim();
if (flick) { dragServe(flick); return; } // dragServe handles GUN (burst) and the live-ball banish
}
if (G.phase === 'pretoss' && score.server === 'P') {
if (tryGunBurst()) return; // GUN selected: the serve gesture pops a burst, no ball
if (G.mode === 'splatter') {
const flick = takeFlickAim();
if (flick) { // flick = a quick aimed serve; a plain tap still does the full toss ritual
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;
tossBall(P); return; // plain tap: the two-stage toss ritual (tennis, and the duel)
}
if (G.phase === 'toss' && score.server === 'P') {
// serve timing: distance from apex = quality — a real serve, not a free tap
@@ -585,7 +586,7 @@ export function keyPressed(code) {
if (dy < 0.12) q = 1;
G.wasServe = true;
// a flick at the apex (or held arrows) makes this a DELIBERATE serve: aimed + consumes a special
const flick = G.mode === 'splatter' ? takeFlickAim() : null;
const flick = G.mode === 'splatter' && !G.interlude ? takeFlickAim() : null;
const kb = { ax: axisX(), az: axisZ() };
hitServe(P, q, flick || (kb.ax || kb.az ? kb : null));
return;
@@ -658,6 +659,9 @@ export function update(dt) {
if (mx) P.facing = mx > 0 ? 1 : -1;
else if (Math.abs(assist) > 0.25) P.facing = assist > 0 ? 1 : -1; // hysteresis: small corrections don't flip facing
}
// splatter: you hold the baseline (a turret) — only side-to-side, no drifting up-court.
// The duel is exempt: it's tennis again, so depth is yours to chase the rally.
if (G.mode === 'splatter' && !G.interlude) P.z = HOME.P.z;
if (P.swingT >= 0) {
P.swingT += dt;
if (G.mode === 'tennis' && ball.active && ball.lastHit === 'O') { // sample the approach for the miss log (t = what trySwing tests this frame)