Phase 4: bomb balls
Ball boys deliver a bomb ball every TUNE.bombEvery (8) splatter kills, up to TUNE.bombMax (3) in stock (G.bombs / G.killMeter in state). The next serve auto-consumes one (hitServe, splatter-only) — no extra input, keeping one-handed play. A bomb detonates on first zombie contact (regardless of pace) or, if it misses everyone, on its first bounce: explode() splatters everything within TUNE.bombRadius (2.5m), each kill feeding ball.kills so the Phase-2 fanfare escalates naturally, and leaves a crater — big stain, size-3 pile, 40 particles, 0.1s hitstop, boom(). New synth boom() in audio.js. Bomb ball renders black with a flickering orange fuse; the HUD shows stocked bombs as black/orange pips by the YOU line. Both explode sites set phase='over' so the serve loop doesn't stall on the killed ball. Alternative delivery ideas Oliver floated but not built: umpire throws it, or court pickup. Verified via window.T: 8th kill earns a bomb (meter resets); a serve consumes one and renders #111 black + orange fuse; a pow-0 bomb amid 4 clustered zombies kills all 4 with 'MASSACRE ×4', boom, hitstop 0.1, crater stain (r1.4) + size-4 pile; a bomb that misses still craters on first bounce; bombs never leak into tennis (mode guard); 3 HUD pips; no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
ad4c8041b9
commit
06025bdccf
6 files changed
+67
-5
No files matched your search
@@ -90,11 +90,16 @@ function drawShadow(px, py, rx, ry, alpha = 0.28) {
|
||||
ctx.fillStyle = `rgba(0,0,0,${alpha})`;
|
||||
ctx.beginPath(); ctx.ellipse(px, py, rx, Math.max(1, ry), 0, 0, 7); ctx.fill();
|
||||
}
|
||||
function drawBall(x, y, z, glint = false) {
|
||||
function drawBall(x, y, z, glint = false, bomb = false) {
|
||||
const sh = proj(x, 0, z);
|
||||
drawShadow(sh.x, sh.y, Math.max(1.5, sh.s * 0.09), sh.s * 0.04, 0.3);
|
||||
const bp = proj(x, y, z);
|
||||
const r = Math.max(1.5, bp.s * 0.09);
|
||||
if (bomb) { // black ball with a flickering orange fuse pixel on top
|
||||
ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(bp.x, bp.y, r, 0, 7); ctx.fill();
|
||||
if (Math.floor(G.idleT * 20) % 2) { ctx.fillStyle = '#ff8010'; ctx.fillRect(bp.x - 0.5, bp.y - r - 1, Math.max(1, r * 0.5), Math.max(1, r * 0.6)); }
|
||||
return;
|
||||
}
|
||||
ctx.fillStyle = '#e8f03a'; ctx.beginPath(); ctx.arc(bp.x, bp.y, r, 0, 7); ctx.fill();
|
||||
if (glint) { ctx.fillStyle = '#f8f8d0'; ctx.fillRect(bp.x - r * 0.3, bp.y - r * 0.5, Math.max(1, r * 0.5), Math.max(1, r * 0.5)); }
|
||||
}
|
||||
@@ -289,7 +294,7 @@ export function render() {
|
||||
drawBall(bx, by, sv.z);
|
||||
}
|
||||
// the match ball
|
||||
if ((ball.active || G.phase === 'over') && !ball.collected) drawBall(ball.x, ball.y, ball.z, true);
|
||||
if ((ball.active || G.phase === 'over') && !ball.collected) drawBall(ball.x, ball.y, ball.z, true, ball.bomb);
|
||||
// splatter particles
|
||||
for (const pt of parts) {
|
||||
const pp = proj(pt.x, pt.y, pt.z);
|
||||
@@ -307,6 +312,10 @@ export function render() {
|
||||
: ['0', '15', '30', '40'][Math.min(score.pts[i], 3)];
|
||||
ctx.fillStyle = score.server === 'P' ? '#f8e048' : '#c8d0dc';
|
||||
ctx.fillText('YOU ' + score.games[0] + ' ' + ptTxt(0), 12, 10);
|
||||
for (let b = 0; b < G.bombs; b++) { // stocked bomb balls: black pips with an orange fuse rim
|
||||
ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(96 + b * 8, 14, 2.6, 0, 7); ctx.fill();
|
||||
ctx.strokeStyle = '#ff8010'; ctx.lineWidth = 0.75; ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = score.server === 'O' ? '#f8e048' : '#c8d0dc';
|
||||
ctx.fillText((G.mode === 'tennis' ? 'CPU ' : 'THEM ') + score.games[1] + ' ' + ptTxt(1), 12, 22);
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
Reference in new issue
Block a user