diff --git a/assets/_sheet_cwound1.png b/assets/_sheet_cwound1.png index d215102..2009851 100644 Binary files a/assets/_sheet_cwound1.png and b/assets/_sheet_cwound1.png differ diff --git a/assets/_sheet_gwound.png b/assets/_sheet_gwound.png index 89a0486..fe8babe 100644 Binary files a/assets/_sheet_gwound.png and b/assets/_sheet_gwound.png differ diff --git a/assets/z_cw1.png b/assets/z_cw1.png index 5ed470e..74b152a 100644 Binary files a/assets/z_cw1.png and b/assets/z_cw1.png differ diff --git a/assets/z_gw.png b/assets/z_gw.png index d485333..fc9dd34 100644 Binary files a/assets/z_gw.png and b/assets/z_gw.png differ diff --git a/gameplay.js b/gameplay.js index 8616010..ee3bad7 100644 --- a/gameplay.js +++ b/gameplay.js @@ -567,15 +567,19 @@ 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. + // splatter serve stays a TWO-STAGE ritual — toss up, THEN fire — but the toss can start any + // time, even with a ball still in flight: the old ball flies on as a ballistic clone so the + // toss+fire pair chains without waiting for it to leave. A flick is the aimed one-motion + // variant (the reticle). 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 - // a bare tap MID-RALLY serves at the auto-target too — pump the screen to keep the balls - // coming while the last one's still up. (pretoss keeps its toss ritual, handled below.) - if (G.phase === 'rally') { dragServe(null); return; } + if (G.phase === 'rally') { // a tap mid-rally re-arms the TOSS (stage one); the next tap fires + if (tryGunBurst()) return; + if (ball.active) 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 }); + tossBall(P); return; + } } if (G.phase === 'pretoss' && score.server === 'P') { if (tryGunBurst()) return; // GUN selected: the serve gesture pops a burst, no ball diff --git a/zombies.js b/zombies.js index 78d8238..71fdf33 100644 --- a/zombies.js +++ b/zombies.js @@ -155,22 +155,23 @@ export function explode(x, z) { if (G.idleT - G.lastOrderT > 3) { voice('order', 0.35); G.lastOrderT = G.idleT; } // the umpire loses composure } -// wounded zombies bleed from every open wound — a thin, steady trickle (a spurt from the -// neck stump on the headless boss). Anchors are approximate body-height offsets [x, y, rise, -// count] (drawStatic never mirrors, so a fixed side stays put); the parts loop's gravity arcs -// each drop. Tight spread keeps the streams thin; bounded so it stays subtle. +// wounded zombies bleed from every open wound — a fine, sparse trickle (a small spurt from the +// neck stump on the headless boss). Anchors are approximate body offsets [x, y, rise]; most +// frames a wound drips nothing, and the tight spread keeps each thread thin. Bounded low. function bleed(zb) { - if (parts.length > 220) return; + if (parts.length > 90) return; let wounds; - if (zb.variant === 'g' && zb.hp < TUNE.bruteHp) wounds = [[-0.3, 1.95, 1.8, 1]]; // brute: torn-off arm - else if (zb.variant === 'r' && zb.hp <= 1) wounds = [[0.05, 1.5, 3.6, 1], [0.4, 1.15, 1.4, 1]]; // headless: neck spurt + torn arm - else if (zb.variant === 'r' && zb.hp === 2) wounds = [[0.4, 1.2, 1.6, 1]]; // racket: torn-off arm + if (zb.variant === 'g' && zb.hp < TUNE.bruteHp) wounds = [[-0.3, 1.95, 1.1]]; // brute: torn-off arm + else if (zb.variant === 'r' && zb.hp <= 1) wounds = [[0.05, 1.5, 2.4], [0.4, 1.15, 0.9]]; // headless: neck spurt + torn arm + else if (zb.variant === 'r' && zb.hp === 2) wounds = [[0.4, 1.2, 1.0]]; // racket: torn-off arm else return; - for (const w of wounds) - for (let k = 0; k < w[3]; k++) parts.push({ - x: zb.x + w[0] + (Math.random() * 2 - 1) * 0.05, y: w[1] + (Math.random() * 2 - 1) * 0.04, z: zb.z + (Math.random() * 2 - 1) * 0.05, - vx: (Math.random() * 2 - 1) * 0.6, vy: w[2] * (0.6 + Math.random() * 0.6), vz: (Math.random() * 2 - 1) * 0.5, - t: 0.4 + Math.random() * 0.35, dark: Math.random() < 0.5 }); + for (const w of wounds) { + if (Math.random() > 0.4) continue; // sparse: most frames this wound emits nothing + parts.push({ + x: zb.x + w[0] + (Math.random() * 2 - 1) * 0.03, y: w[1] + (Math.random() * 2 - 1) * 0.03, z: zb.z + (Math.random() * 2 - 1) * 0.03, + vx: (Math.random() * 2 - 1) * 0.3, vy: w[2] * (0.6 + Math.random() * 0.5), vz: (Math.random() * 2 - 1) * 0.28, + t: 0.3 + Math.random() * 0.3, dark: Math.random() < 0.55 }); + } } export function updateZombies(dt) {