diff --git a/assets/audio/double_kill.m4a b/assets/audio/double_kill.m4a new file mode 100644 index 0000000..cf519fd Binary files /dev/null and b/assets/audio/double_kill.m4a differ diff --git a/assets/audio/triple_kill.m4a b/assets/audio/triple_kill.m4a new file mode 100644 index 0000000..e18e9f7 Binary files /dev/null and b/assets/audio/triple_kill.m4a differ diff --git a/assets/audio/unbelievable.m4a b/assets/audio/unbelievable.m4a new file mode 100644 index 0000000..394b9d4 Binary files /dev/null and b/assets/audio/unbelievable.m4a differ diff --git a/config.js b/config.js index c71b01a..b4c98ad 100644 --- a/config.js +++ b/config.js @@ -23,6 +23,7 @@ export const TUNE = { zSplatQ: 0.5, // min shot quality to splatter; below = stagger pushback zPushback: 1.4, // m a staggered zombie is knocked back splatterOverT: 0.1, // s the spent-ball 'over' beat lasts before the next serve is armed + zPierce: 0.75, // pace a ball keeps after a splatter — hot balls plow through lined-up pairs missHud: 1, // show the last miss reason on screen (full log: T.G.missLog) }; diff --git a/gameplay.js b/gameplay.js index 948798d..0a41e28 100644 --- a/gameplay.js +++ b/gameplay.js @@ -78,6 +78,7 @@ function doHit(ch, quality, aim) { ball.lastHit = ch.side; ball.bounces = 0; ball.firstLand = null; G.wasServe = false; // a serve volleyed back pre-bounce must not fault-check the return ball.pow = quality; // carried pace: zombies splatter only above zSplatQ + ball.kills = 0; // fresh shot: multi-kill streak restarts if (ch.side === 'P') { // shot marker: where this will land — red means it's going out const ok = G.mode !== 'tennis' || (Math.abs(tx) <= COURT.W / 2 + 0.02 && tz >= -0.02 && tz <= COURT.netZ); G.outMarker = { x: tx, z: tz, out: !ok, t: 1.3 }; @@ -118,6 +119,7 @@ function hitServe(sv, quality, aim = null) { ball.vx = v.vx; ball.vy = v.vy; ball.vz = v.vz; ball.lastHit = sv.side; ball.bounces = 0; ball.pow = quality; + ball.kills = 0; // fresh serve: multi-kill streak restarts if (sv.side === 'P') G.outMarker = { x: boxX, z: tzs, out: G.mode === 'tennis' && !inServeBox(boxX, tzs), t: 1.3 }; G.phase = 'rally'; pock(1, 1100); sv.pose = 'serve'; sv.poseT = 0; @@ -136,7 +138,7 @@ function scoreKey() { return N[s[0]] + '_' + N[s[1]]; } -function cheer(seconds, vol) { +export function cheer(seconds, vol) { applause(seconds, vol); crowd.cheer = Math.max(crowd.cheer, seconds); } diff --git a/state.js b/state.js index 5768334..5ab3be0 100644 --- a/state.js +++ b/state.js @@ -5,7 +5,7 @@ import { HOME } from './config.js'; export const P = { x: 1.3, z: 22.8, side: 'P', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 }; export const O = { x: -1.3, z: 1.0, side: 'O', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 }; export const ball = { x: 0, y: 1, z: 20, vx: 0, vy: 0, vz: 0, g: 9.2, - active: false, lastHit: 'P', bounces: 0, firstLand: null, collected: false }; + active: false, lastHit: 'P', bounces: 0, firstLand: null, collected: false, kills: 0 }; export const score = { pts: [0, 0], games: [0, 0], server: 'P' }; // pts index 0=P 1=O export const crowd = { cheer: 0, dots: [] }; // dots are (re)built by render.buildCrowd diff --git a/zombies.js b/zombies.js index 13a255b..6e89544 100644 --- a/zombies.js +++ b/zombies.js @@ -6,7 +6,7 @@ import { TUNE, COURT } from './config.js'; import { G, P, ball, sendBoy } from './state.js'; import { splat, moan, pock, voice } from './audio.js'; -import { splatPoint } from './gameplay.js'; +import { splatPoint, cheer } from './gameplay.js'; export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'} export const gore = []; // lawn decals {x, z, r, a} @@ -22,6 +22,7 @@ export function spawnZombie(x, z, variant) { function splatter(i) { const zb = zombies[i]; zombies.splice(i, 1); + ball.kills++; // this shot's running streak gore.push({ x: zb.x, z: zb.z, r: 0.45 + Math.random() * 0.35, a: 0.85 }); if (gore.length > 40) gore.shift(); for (let k = 0; k < 14; k++) parts.push({ @@ -30,9 +31,13 @@ function splatter(i) { t: 0.4 + Math.random() * 0.4, }); splat(); - ball.vx *= 0.5; ball.vz *= 0.5; // the kill takes pace off the ball + ball.vx *= TUNE.zPierce; ball.vz *= TUNE.zPierce; // keep enough pace to plow through the next sendBoy(zb.x, zb.z); // a boy sprints out to mop splatPoint('P'); + // escalating multi-kill fanfare — one shot, ≥2 zombies + if (ball.kills === 2) { G.flash = { text: 'DOUBLE KILL', t: 0.9 }; cheer(1.6, 0.45); voice('double_kill', 0.4); } + else if (ball.kills === 3) { G.flash = { text: 'TRIPLE KILL', t: 0.9 }; cheer(2.2, 0.6); voice('triple_kill', 0.4); } + else if (ball.kills >= 4) { G.flash = { text: `MASSACRE ×${ball.kills}`, t: 0.9 }; cheer(2.8, 0.7); voice('unbelievable', 0.4); } } export function updateZombies(dt) {