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:
1 parent
bf9553bd49
commit
9100bd5a23
5 files changed
+84
-23
No files matched your search
@@ -30,6 +30,7 @@ export const TUNE = {
|
|||||||
hordeLen: 12, // s a horde wave lasts
|
hordeLen: 12, // s a horde wave lasts
|
||||||
hordeRate: 2.5, // zombies spawned per second during a horde
|
hordeRate: 2.5, // zombies spawned per second during a horde
|
||||||
mgRate: 6, // machine-gun pellets fired per second during a horde
|
mgRate: 6, // machine-gun pellets fired per second during a horde
|
||||||
|
meleeRange: 1.7, // m — a racket swish cleaves shamblers this close to you
|
||||||
missHud: 1, // show the last miss reason on screen (full log: T.G.missLog)
|
missHud: 1, // show the last miss reason on screen (full log: T.G.missLog)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+9
-11
@@ -4,7 +4,7 @@ import { TUNE, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DE
|
|||||||
import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy } from './state.js';
|
import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy } from './state.js';
|
||||||
import { audio, pock, swish, applause, voice } from './audio.js';
|
import { audio, pock, swish, applause, voice } from './audio.js';
|
||||||
import { axisX, axisZ, takeFlickAim } from './input.js';
|
import { axisX, axisZ, takeFlickAim } from './input.js';
|
||||||
import { updateZombies, spawnZombie, zombies, explode } from './zombies.js';
|
import { updateZombies, spawnZombie, zombies, explode, meleeSwipe, stashExtraBall } from './zombies.js';
|
||||||
|
|
||||||
// ------------------------------------------------------------- ballistics ----
|
// ------------------------------------------------------------- ballistics ----
|
||||||
// Launch ball from (x0,y0,z0) so it lands at (tx, tz) with horizontal speed s under gravity g.
|
// Launch ball from (x0,y0,z0) so it lands at (tx, tz) with horizontal speed s under gravity g.
|
||||||
@@ -375,22 +375,20 @@ function trySwing(ch, isAI) {
|
|||||||
export function keyPressed(code) {
|
export function keyPressed(code) {
|
||||||
if (code !== 'Space') return;
|
if (code !== 'Space') return;
|
||||||
audio(); // unlock on first gesture
|
audio(); // unlock on first gesture
|
||||||
if (G.horde) return; // horde: the racket auto-fires, taps do nothing — the thumb drag steers you
|
|
||||||
// splatter: a tap/flick during the brief 'over' beat arms the next serve immediately —
|
|
||||||
// reset inline, then fall through to pretoss so the finger is the only cadence limit
|
|
||||||
if (G.mode === 'splatter' && G.phase === 'over') { ball.active = false; setupPoint(); }
|
|
||||||
if (G.phase === 'pretoss' && score.server === 'P') {
|
|
||||||
if (G.mode === 'splatter') {
|
if (G.mode === 'splatter') {
|
||||||
const flick = takeFlickAim();
|
// splatter is a shooter: every swish cleaves adjacent shamblers, and — outside a horde —
|
||||||
if (flick) { // quick serve: no toss ritual, modest pace, aimed by the flick
|
// fires a ball immediately. Serving while one still flies stashes it, so you can barrage.
|
||||||
|
meleeSwipe(P.x, P.z, TUNE.meleeRange);
|
||||||
|
if (G.horde) return; // horde: MG auto-fires, the tap only swishes (no serve)
|
||||||
|
if (ball.active) stashExtraBall(); // keep the in-flight ball alive
|
||||||
|
const flick = takeFlickAim(); // flick aims; a plain tap auto-targets the nearest zombie
|
||||||
ball.active = true; ball.collected = false; ball.bounces = 0; ball.firstLand = null;
|
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;
|
ball.x = P.x + 0.35; ball.z = P.z; ball.y = 2.2; ball.g = TUNE.gravity;
|
||||||
hitServe(P, 0.62, flick);
|
hitServe(P, 0.62, flick);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
// tennis: the toss ritual + rally swing (unchanged)
|
||||||
tossBall(P); return;
|
if (G.phase === 'pretoss' && score.server === 'P') { tossBall(P); return; }
|
||||||
}
|
|
||||||
if (G.phase === 'toss' && score.server === 'P') {
|
if (G.phase === 'toss' && score.server === 'P') {
|
||||||
// serve timing: distance from apex = quality
|
// serve timing: distance from apex = quality
|
||||||
const dy = Math.abs(ball.y - serveApexY());
|
const dy = Math.abs(ball.y - serveApexY());
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { P, O, ball, score, G, setupPoint } from './state.js';
|
|||||||
import { update, keyPressed, solveShot, inServeBox, inCourt, forceZombies } from './gameplay.js';
|
import { update, keyPressed, solveShot, inServeBox, inCourt, forceZombies } from './gameplay.js';
|
||||||
import { fitViewport, render, proj } from './render.js';
|
import { fitViewport, render, proj } from './render.js';
|
||||||
import { voice } from './audio.js';
|
import { voice } from './audio.js';
|
||||||
import { zombies, spawnZombie, piles, gore, parts, pellets } from './zombies.js';
|
import { zombies, spawnZombie, piles, gore, parts, pellets, extraBalls } from './zombies.js';
|
||||||
import { cv } from './dom.js';
|
import { cv } from './dom.js';
|
||||||
import './input.js';
|
import './input.js';
|
||||||
|
|
||||||
@@ -54,6 +54,6 @@ cv.addEventListener('pointerdown', e => {
|
|||||||
requestAnimationFrame(frame);
|
requestAnimationFrame(frame);
|
||||||
|
|
||||||
// exposed for in-browser sanity checks and simulated playtests
|
// exposed for in-browser sanity checks and simulated playtests
|
||||||
window.T = { TUNE, ball, P, O, score, G, zombies, spawnZombie, piles, gore, parts, pellets, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies,
|
window.T = { TUNE, ball, P, O, score, G, zombies, spawnZombie, piles, gore, parts, pellets, extraBalls, solveShot, inServeBox, inCourt, proj, setupPoint, forceZombies,
|
||||||
step: dt => update(dt), render, press: keyPressed,
|
step: dt => update(dt), render, press: keyPressed,
|
||||||
get phase() { return G.phase; }, set phase(v) { G.phase = v; } };
|
get phase() { return G.phase; }, set phase(v) { G.phase = v; } };
|
||||||
@@ -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 { 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 { P, O, ball, score, crowd, G } from './state.js';
|
||||||
import { predictLanding } from './gameplay.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 ----
|
// ---------------------------------------------------------------- palette ----
|
||||||
// tennis palette -> drained splatter palette; the transition lerps between them
|
// 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 = 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);
|
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 ----
|
// ------------------------------------------------------------------ frame ----
|
||||||
export function render() {
|
export function render() {
|
||||||
@@ -293,6 +304,8 @@ export function render() {
|
|||||||
const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * BOUNCE_IDLE.apex;
|
const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * BOUNCE_IDLE.apex;
|
||||||
drawBall(bx, by, sv.z);
|
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
|
// the match ball
|
||||||
if ((ball.active || G.phase === 'over') && !ball.collected) drawBall(ball.x, ball.y, ball.z, true, ball.bomb);
|
if ((ball.active || G.phase === 'over') && !ball.collected) drawBall(ball.x, ball.y, ball.z, true, ball.bomb);
|
||||||
// splatter particles
|
// splatter particles
|
||||||
@@ -308,6 +321,7 @@ export function render() {
|
|||||||
ctx.fillStyle = '#f8e048'; ctx.fillRect((pp.x | 0) - 1, (pp.y | 0) - 1, 2, 2);
|
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.mode !== 'eaten') drawChar(P); // there is no P anymore
|
||||||
|
if (G.horde && G.mode === 'splatter') drawMgRacket(); // rambo racket over the player
|
||||||
|
|
||||||
// HUD — scoreboard
|
// HUD — scoreboard
|
||||||
ctx.fillStyle = 'rgba(12,16,24,0.82)'; ctx.fillRect(6, 6, 132, 30);
|
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) {
|
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde) {
|
||||||
ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace';
|
ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace';
|
||||||
const touch = matchMedia('(pointer: coarse)').matches;
|
const touch = matchMedia('(pointer: coarse)').matches;
|
||||||
ctx.fillText(G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
|
const txt = G.mode === 'splatter'
|
||||||
: (touch ? 'TAP TO TOSS, TAP TO SERVE' : 'SPACE TO TOSS, SPACE TO SERVE'), W / 2, H - 16);
|
? (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) {
|
if (G.flash) {
|
||||||
ctx.font = 'bold 14px monospace'; ctx.fillStyle = '#ffffff';
|
ctx.font = 'bold 14px monospace'; ctx.fillStyle = '#ffffff';
|
||||||
|
|||||||
+52
-7
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { TUNE, COURT } from './config.js';
|
import { TUNE, COURT } from './config.js';
|
||||||
import { G, P, ball, sendBoy, setupPoint } from './state.js';
|
import { G, P, ball, sendBoy, setupPoint } from './state.js';
|
||||||
import { splat, moan, pock, voice, boom } from './audio.js';
|
import { splat, moan, pock, voice, boom, swish } from './audio.js';
|
||||||
import { splatPoint, cheer } from './gameplay.js';
|
import { splatPoint, cheer } from './gameplay.js';
|
||||||
|
|
||||||
export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'}
|
export const zombies = []; // {x, z, v, walkT, variant: 'a'|'b'|'c'}
|
||||||
@@ -13,6 +13,7 @@ export const gore = []; // lawn decals {x, z, r, a} — the flat ground laye
|
|||||||
export const piles = []; // 3D-ish gore mounds {x, z, size 1..4, seed} — grow where kills cluster
|
export const piles = []; // 3D-ish gore mounds {x, z, size 1..4, seed} — grow where kills cluster
|
||||||
export const parts = []; // splatter particles {x, y, z, vx, vy, vz, t, dark}
|
export const parts = []; // splatter particles {x, y, z, vx, vy, vz, t, dark}
|
||||||
export const pellets = []; // machine-gun rounds during a horde {x, y, z, vx, vz, t}
|
export const pellets = []; // machine-gun rounds during a horde {x, y, z, vx, vz, t}
|
||||||
|
export const extraBalls = []; // earlier serves still in flight while you fire the next {x,y,z,vx,vy,vz,g,pow,bomb,bounces}
|
||||||
|
|
||||||
let spawnT = 1.5, moanT = 3;
|
let spawnT = 1.5, moanT = 3;
|
||||||
|
|
||||||
@@ -21,7 +22,9 @@ export function spawnZombie(x, z, variant) {
|
|||||||
zombies.push({ x, z, v: TUNE.zSpeed * ramp * (0.8 + Math.random() * 0.4), walkT: Math.random() * 9, variant });
|
zombies.push({ x, z, v: TUNE.zSpeed * ramp * (0.8 + Math.random() * 0.4), walkT: Math.random() * 9, variant });
|
||||||
}
|
}
|
||||||
|
|
||||||
function splatter(i, fanfare = true) {
|
// core kill: remove the zombie, spawn its gore/pile/particles, score. No ball coupling —
|
||||||
|
// shared by ball hits, machine-gun pellets, melee swipes, and extra (multi-serve) balls.
|
||||||
|
function killZombie(i, quiet) {
|
||||||
const zb = zombies[i];
|
const zb = zombies[i];
|
||||||
zombies.splice(i, 1);
|
zombies.splice(i, 1);
|
||||||
gore.push({ x: zb.x, z: zb.z, r: 0.45 + Math.random() * 0.35, a: 0.85 });
|
gore.push({ x: zb.x, z: zb.z, r: 0.45 + Math.random() * 0.35, a: 0.85 });
|
||||||
@@ -36,9 +39,32 @@ function splatter(i, fanfare = true) {
|
|||||||
t: dark ? 0.6 + Math.random() * 0.6 : 0.4 + Math.random() * 0.4, dark,
|
t: dark ? 0.6 + Math.random() * 0.6 : 0.4 + Math.random() * 0.4, dark,
|
||||||
}); }
|
}); }
|
||||||
splat();
|
splat();
|
||||||
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
|
sendBoy(zb.x, zb.z); // a boy sprints out to mop
|
||||||
splatPoint('P', !!G.horde); // horde kills rate-limit the umpire so he doesn't machine-gun scores
|
splatPoint('P', quiet); // horde kills rate-limit the umpire so he doesn't machine-gun scores
|
||||||
|
}
|
||||||
|
|
||||||
|
// swishing the racket cleaves any shambler in reach (the racket-zombie boss shrugs melee off)
|
||||||
|
export function meleeSwipe(x, z, range) {
|
||||||
|
let hit = 0;
|
||||||
|
for (let i = zombies.length - 1; i >= 0; i--) {
|
||||||
|
if (zombies[i].variant === 'r') continue;
|
||||||
|
if (Math.hypot(zombies[i].x - x, zombies[i].z - z) < range) { killZombie(i, !!G.horde); hit++; }
|
||||||
|
}
|
||||||
|
if (hit) swish(0.35);
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// serving again while a ball still flies: stash the current one so it keeps killing (a barrage)
|
||||||
|
export function stashExtraBall() {
|
||||||
|
if (!ball.active) return;
|
||||||
|
extraBalls.push({ x: ball.x, y: ball.y, z: ball.z, vx: ball.vx, vy: ball.vy, vz: ball.vz,
|
||||||
|
g: ball.g, pow: ball.pow, bomb: ball.bomb, bounces: ball.bounces });
|
||||||
|
if (extraBalls.length > 6) extraBalls.shift(); // cap the barrage
|
||||||
|
}
|
||||||
|
|
||||||
|
function splatter(i, fanfare = true) { // the MAIN ball hit a zombie
|
||||||
|
killZombie(i, !!G.horde);
|
||||||
|
ball.vx *= TUNE.zPierce; ball.vz *= TUNE.zPierce; // keep enough pace to plow through the next
|
||||||
if (!fanfare) return; // machine-gun pellets splatter but skip the streak fanfare / bomb meter
|
if (!fanfare) return; // machine-gun pellets splatter but skip the streak fanfare / bomb meter
|
||||||
ball.kills++; // this shot's running streak
|
ball.kills++; // this shot's running streak
|
||||||
// escalating multi-kill fanfare — one shot, ≥2 zombies
|
// escalating multi-kill fanfare — one shot, ≥2 zombies
|
||||||
@@ -111,7 +137,10 @@ export function updateZombies(dt) {
|
|||||||
const zb = zombies[i];
|
const zb = zombies[i];
|
||||||
zb.walkT += dt;
|
zb.walkT += dt;
|
||||||
zb.z += zb.v * dt;
|
zb.z += zb.v * dt;
|
||||||
zb.x += Math.sin(zb.walkT * 1.7) * 0.15 * dt; // shamblers don't walk straight lines
|
if (G.mode === 'splatter' && zb.variant !== 'r' && zb.z > COURT.netZ) // on YOUR half they home in on you
|
||||||
|
zb.x += Math.sign(P.x - zb.x) * Math.min(Math.abs(P.x - zb.x), zb.v * 0.7 * dt);
|
||||||
|
else
|
||||||
|
zb.x += Math.sin(zb.walkT * 1.7) * 0.15 * dt; // far half / racket boss: aimless shamble
|
||||||
if (G.mode === 'splatter' && Math.hypot(zb.x - P.x, zb.z - P.z) < 0.7) { // it got you
|
if (G.mode === 'splatter' && Math.hypot(zb.x - P.x, zb.z - P.z) < 0.7) { // it got you
|
||||||
gore.push({ x: P.x, z: P.z, r: 0.9, a: 0.9 });
|
gore.push({ x: P.x, z: P.z, r: 0.9, a: 0.9 });
|
||||||
for (let k = 0; k < 30; k++) parts.push({
|
for (let k = 0; k < 30; k++) parts.push({
|
||||||
@@ -122,7 +151,7 @@ export function updateZombies(dt) {
|
|||||||
splat(1);
|
splat(1);
|
||||||
voice('game_set_massacre', 0.8);
|
voice('game_set_massacre', 0.8);
|
||||||
G.mode = 'eaten'; ball.active = false; G.flash = null; // zombies keep shambling for ambience
|
G.mode = 'eaten'; ball.active = false; G.flash = null; // zombies keep shambling for ambience
|
||||||
G.horde = null; pellets.length = 0; // the swarm stops; the eaten screen takes over
|
G.horde = null; pellets.length = 0; extraBalls.length = 0; // the swarm stops; the eaten screen takes over
|
||||||
}
|
}
|
||||||
if (ball.active && ball.lastHit === 'P' && ball.y < 1.9) {
|
if (ball.active && ball.lastHit === 'P' && ball.y < 1.9) {
|
||||||
const d = Math.hypot(ball.x - zb.x, ball.z - zb.z);
|
const d = Math.hypot(ball.x - zb.x, ball.z - zb.z);
|
||||||
@@ -145,10 +174,26 @@ export function updateZombies(dt) {
|
|||||||
pe.t -= dt; pe.x += pe.vx * dt; pe.z += pe.vz * dt;
|
pe.t -= dt; pe.x += pe.vx * dt; pe.z += pe.vz * dt;
|
||||||
let hit = false;
|
let hit = false;
|
||||||
for (let j = zombies.length - 1; j >= 0; j--)
|
for (let j = zombies.length - 1; j >= 0; j--)
|
||||||
if (Math.hypot(zombies[j].x - pe.x, zombies[j].z - pe.z) < 0.55) { splatter(j, false); hit = true; break; }
|
if (zombies[j].variant !== 'r' && Math.hypot(zombies[j].x - pe.x, zombies[j].z - pe.z) < 0.55) { killZombie(j, true); hit = true; break; }
|
||||||
if (hit || pe.t <= 0) pellets.splice(i, 1);
|
if (hit || pe.t <= 0) pellets.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = extraBalls.length - 1; i >= 0; i--) { // earlier serves keep arcing and killing
|
||||||
|
const b = extraBalls[i];
|
||||||
|
b.vy -= (b.g || TUNE.gravity) * dt;
|
||||||
|
b.x += b.vx * dt; b.y += b.vy * dt; b.z += b.vz * dt;
|
||||||
|
let done = false;
|
||||||
|
if (b.y <= 0 && b.vy < 0) { b.y = 0; b.vy = -b.vy * TUNE.bounceY; b.vx *= TUNE.bounceXZ; b.vz *= TUNE.bounceXZ; b.bounces++;
|
||||||
|
if (b.bomb) { explode(b.x, b.z); done = true; } }
|
||||||
|
if (!done && b.y < 1.9) for (let j = zombies.length - 1; j >= 0; j--) {
|
||||||
|
if (zombies[j].variant === 'r' || Math.hypot(zombies[j].x - b.x, zombies[j].z - b.z) >= 0.55) continue;
|
||||||
|
if (b.bomb) { explode(b.x, b.z); done = true; }
|
||||||
|
else if ((b.pow || 0) >= TUNE.zSplatQ) { killZombie(j, !!G.horde); b.vx *= TUNE.zPierce; b.vz *= TUNE.zPierce; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (done || b.bounces >= 2 || b.z < -2 || b.z > 25.5 || Math.abs(b.x) > 7.5) extraBalls.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = parts.length - 1; i >= 0; i--) {
|
for (let i = parts.length - 1; i >= 0; i--) {
|
||||||
const p = parts[i];
|
const p = parts[i];
|
||||||
p.t -= dt; p.vy -= 14 * dt;
|
p.t -= dt; p.vy -= 14 * dt;
|
||||||
|
|||||||
Reference in new issue
Block a user