Splatter aiming targets zombies anywhere; triple-tap the umpire to skip ahead
- doHit/hitServe in splatter mode lock onto zombies instead of tennis court depths: your flick picks the lane and depth band, the shot targets the best match anywhere on the court (including your own side) with a walking lead; serves auto-track the most advanced shambler - hidden shortcut: tap/click the umpire three times within 1.6s during tennis mode to summon the transition immediately Verified: near-side and far zombies killable, cheat triggers transition, tennis mode targeting untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
69e3a0c310
commit
60c5f72a0a
2 files changed
+65
-11
No files matched your search
+48
-9
@@ -4,7 +4,7 @@ import { TUNE, COURT, CONTACT, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POS
|
||||
import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy } from './state.js';
|
||||
import { audio, pock, swish, applause, voice } from './audio.js';
|
||||
import { axisX, axisZ, takeFlickAim } from './input.js';
|
||||
import { updateZombies, spawnZombie } from './zombies.js';
|
||||
import { updateZombies, spawnZombie, zombies } from './zombies.js';
|
||||
|
||||
// ------------------------------------------------------------- ballistics ----
|
||||
// Launch ball from (x0,y0,z0) so it lands at (tx, tz) with horizontal speed s under gravity g.
|
||||
@@ -21,6 +21,22 @@ export function predictLanding() { // first-bounce point, closed form — exact,
|
||||
return { x: ball.x + ball.vx * t, z: ball.z + ball.vz * t };
|
||||
}
|
||||
|
||||
// splatter aiming: your flick picks a lane (ax) and depth band (az); the shot locks onto
|
||||
// the best-matching zombie anywhere on the court — including your own side of the net.
|
||||
function zombieTarget(aim) {
|
||||
if (!zombies.length) return null;
|
||||
const lane = (aim ? aim.ax : 0) * 3.3;
|
||||
const band = aim && aim.az !== 0 ? (aim.az < 0 ? 4 : 19) : null;
|
||||
let best = null, bs = 1e9;
|
||||
for (const zb of zombies) {
|
||||
let s = Math.abs(zb.x - lane);
|
||||
if (band !== null) s += Math.abs(zb.z - band) * 0.5;
|
||||
if (!aim) s = COURT.L - zb.z; // no aim (serves): the nearest one is the most urgent
|
||||
if (s < bs) { bs = s; best = zb; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function doHit(ch, quality, aim) {
|
||||
// aim: {ax: -1|0|1, az: -1|0|1} from held keys / flick at contact (az -1 = deep, 1 = short)
|
||||
const toFar = ch.side === 'P';
|
||||
@@ -28,11 +44,19 @@ function doHit(ch, quality, aim) {
|
||||
ball.x = ch.x + ch.facing * 0.5;
|
||||
ball.z = ch.z + (toFar ? -0.3 : 0.3);
|
||||
const err = () => (Math.random() * 2 - 1) * TUNE.aimError * (1 - quality);
|
||||
let tx = aim.ax * (toFar ? 3.3 : 2.7) + err(); // your aimed shots go properly wide
|
||||
const tzBase = aim.az < 0 ? SHOT_DEPTH.deep : aim.az > 0 ? SHOT_DEPTH.short : SHOT_DEPTH.mid;
|
||||
const tz = (toFar ? tzBase : COURT.L - tzBase) + err() * 0.8;
|
||||
tx = Math.max(-5, Math.min(5, tx));
|
||||
let tx, tz;
|
||||
let s = TUNE.baseShotSpeed + quality * TUNE.qualityShotSpeed;
|
||||
const zt = G.mode !== 'tennis' && ch.side === 'P' ? zombieTarget(aim) : null;
|
||||
if (zt) {
|
||||
tx = zt.x + err() * 0.4;
|
||||
const T0 = Math.hypot(zt.x - ball.x, zt.z - ball.z) / s;
|
||||
tz = Math.min(23, zt.z + zt.v * T0) + err() * 0.3; // lead the shambler's walk
|
||||
} else {
|
||||
tx = aim.ax * (toFar ? 3.3 : 2.7) + err(); // your aimed shots go properly wide
|
||||
const tzBase = aim.az < 0 ? SHOT_DEPTH.deep : aim.az > 0 ? SHOT_DEPTH.short : SHOT_DEPTH.mid;
|
||||
tz = (toFar ? tzBase : COURT.L - tzBase) + err() * 0.8;
|
||||
}
|
||||
tx = Math.max(-5, Math.min(5, tx));
|
||||
// topspin: quality shots dive under extra effective gravity, which is what lets real
|
||||
// pace arc over the net and still land in — the core tennis trade-off
|
||||
const g = TUNE.gravity * (1 + quality * TUNE.spinFactor);
|
||||
@@ -71,11 +95,19 @@ export function serveApexY() { return TOSS.y0 + TOSS.vy * TOSS.vy / (2 * TUNE.gr
|
||||
|
||||
function hitServe(sv, quality) {
|
||||
const toFar = sv.side === 'P';
|
||||
// target: diagonal service box. Serving from deuce side → target box is across.
|
||||
const boxX = (deuceSide() ? -1 : 1) * (toFar ? 1 : -1) * 2.0 + (Math.random() * 2 - 1) * TUNE.aimError * 0.8 * (1 - quality);
|
||||
const boxZ = toFar ? COURT.svcFar + 1.6 : COURT.svcNear - 1.6;
|
||||
const s = TUNE.serveSpeed + quality * TUNE.serveQualitySpeed;
|
||||
const tzs = boxZ + (Math.random() * 2 - 1) * (1 - quality) * 1.5;
|
||||
let boxX, tzs;
|
||||
const zt = G.mode !== 'tennis' ? zombieTarget(null) : null;
|
||||
if (zt) { // splatter serves auto-track the most advanced zombie
|
||||
boxX = zt.x + (Math.random() * 2 - 1) * (1 - quality) * 0.8;
|
||||
const T0 = Math.hypot(zt.x - ball.x, zt.z - ball.z) / s;
|
||||
tzs = Math.min(23, zt.z + zt.v * T0);
|
||||
} else {
|
||||
// target: diagonal service box. Serving from deuce side → target box is across.
|
||||
boxX = (deuceSide() ? -1 : 1) * (toFar ? 1 : -1) * 2.0 + (Math.random() * 2 - 1) * TUNE.aimError * 0.8 * (1 - quality);
|
||||
const boxZ = toFar ? COURT.svcFar + 1.6 : COURT.svcNear - 1.6;
|
||||
tzs = boxZ + (Math.random() * 2 - 1) * (1 - quality) * 1.5;
|
||||
}
|
||||
const g = TUNE.gravity * (1 + quality * 0.35); // kick serves dip a little too
|
||||
const v = solveShot(ball.x, ball.y, ball.z, boxX, tzs, s, g);
|
||||
ball.g = g;
|
||||
@@ -144,6 +176,13 @@ export function splatPoint(side) {
|
||||
}
|
||||
}
|
||||
|
||||
// hidden shortcut: tapping the umpire three times summons them early
|
||||
export function forceZombies() {
|
||||
if (G.mode !== 'tennis') return;
|
||||
ball.active = false;
|
||||
startTransition();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- transition ----
|
||||
// You won your first game. The applause dies. The match... continues.
|
||||
function startTransition() {
|
||||
|
||||
Reference in new issue
Block a user