The menu grows a second step, classic learns difficulty, and the rally feel unifies
Oliver's restructure: mode first, then what the mode asks — classic deals
EASY / MEDIUM / HARD chips, ZOMBIE TENNIS deals two sub-mode posters (ARCADE,
the exhibition ladder as was, and THE TOURNAMENT), and SPLATTER GORE stands
alone as the third top card. config.MENU is the grouping, MODES stays the four
run modes, and pickedMode stays the run-mode id — start(), stats and the
summary never learn the menu got taller. The sub-row sits inset under the
posters; sub-cards are the poster at 2/3 height centred on their scene.
DIFFICULTY is the one real gameplay addition: config.DIFFS scales the three
dials the classic CPU already reads (flub, quality cap, aiSpeed), MEDIUM
pinned by test to the shipped classicFlub/classicQualityMax so the middle chip
IS yesterday's game. Rides on G.diff next to G.classic, so restarts keep it.
AND THE FEEL SPLIT OLIVER REPORTED IS REAL: "in classic tennis I tend to miss
balls way more often" — the match's three contact helpers (matchReach 1.35,
the matchAssist floor, the short-ball chase) were all G.match-gated, so
classic played with a 35% smaller racket and no rescue on the dying drop
shot. rallyFeel() = match || classic now carries all three; gore's opener
keeps the old prologue feel on purpose.
The ARCADE poster took two rolls: the first came back a flat sticker line-up
("very constructed, not artsy"), the second anchors on the zombie poster
itself as a MOOD reference — one challenger lit at the net, the queue receding
into purple haze. Rolls in _refcheck/modebtn. The cup's stats mapping also
joins MODE_MATCH (it banks through endRun as MATCH and read back as GORE).
SPRV 117, main.js v233, 66 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d3bbd8e157
commit
e4c917e23d
7 files changed
+141
-34
No files matched your search
+17
-7
@@ -1056,11 +1056,19 @@ function onBounce() {
|
||||
// which points are reachable, and computing that with autoAssist alone meant a desktop match's
|
||||
// intercept thought NOTHING was reachable and sent the player home every time — he had legs but
|
||||
// his destination was always the baseline centre.
|
||||
export const assistK = () => G.match ? Math.max(TUNE.autoAssist, TUNE.matchAssist) : TUNE.autoAssist;
|
||||
// THE RALLY MODES SHARE ONE PLAYER FEEL (Oliver, 2026-08-14: "in classic tennis I tend to miss
|
||||
// balls way more often — zombie tennis is the good one"). The match's three contact helpers —
|
||||
// assist floor, matchReach, the short-ball chase — were tuned in the match and classic never got
|
||||
// them, so the same swing whiffed centimetres short there. Gore's opener keeps the old feel: its
|
||||
// tennis is a prologue, not the mode.
|
||||
export const rallyFeel = () => !!(G.match || G.classic);
|
||||
export const assistK = () => rallyFeel() ? Math.max(TUNE.autoAssist, TUNE.matchAssist) : TUNE.autoAssist;
|
||||
|
||||
function intercept(side) {
|
||||
const ch = side === 'O' ? O : P;
|
||||
const spd = side === 'O' ? TUNE.aiSpeed : TUNE.playerSpeed * assistK();
|
||||
// G.diff is CLASSIC's difficulty chip (config.DIFFS) — null everywhere else, so only the
|
||||
// classic CPU speeds up or slows down. Same guard on every dial it scales.
|
||||
const spd = side === 'O' ? TUNE.aiSpeed * (G.diff ? G.diff.speed : 1) : TUNE.playerSpeed * assistK();
|
||||
let { x, y, z, vx, vy, vz } = ball;
|
||||
const g = ball.g || TUNE.gravity, dt = 0.01;
|
||||
const inZone = side === 'O' ? zz => zz <= ZONE_DEPTH : zz => zz >= COURT.L - ZONE_DEPTH;
|
||||
@@ -1074,7 +1082,7 @@ function intercept(side) {
|
||||
// legal contact height by then) and the point went to the zombie. Seven of seven recorded
|
||||
// losses. Remember the earliest reachable point ON HIS HALF where the ball is still at hitting
|
||||
// height: running forward onto the short ball is the tennis answer, and now it is the assist's.
|
||||
if (side === 'P' && G.match && !anyHit && i > 5 && z > COURT.netZ && y < 1.6 && y > 0.35) {
|
||||
if (side === 'P' && rallyFeel() && !anyHit && i > 5 && z > COURT.netZ && y < 1.6 && y > 0.35) {
|
||||
const pt = { x, z: Math.min(24.5, z) };
|
||||
if (Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt) anyHit = pt;
|
||||
}
|
||||
@@ -1146,7 +1154,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
// sampled 1.3-1.6 m — centimetres outside the 1.3 m post-bounce reach, point after point. The
|
||||
// assist can deliver you NEAR the ball but not INSIDE a phone-tuned reach; a rally mode lives or
|
||||
// dies on contact feeling fair, and the gore modes are untouched.
|
||||
const reach = isAI ? TUNE.aiReach : TUNE.reach * (volley ? VOLLEY.reach : 1) * (G.match ? TUNE.matchReach : 1);
|
||||
const reach = isAI ? TUNE.aiReach : TUNE.reach * (volley ? VOLLEY.reach : 1) * (rallyFeel() ? TUNE.matchReach : 1);
|
||||
const win = TUNE.swingWindow * (volley ? VOLLEY.window : 1);
|
||||
const t = ch.swingT;
|
||||
if (t < TUNE.swingWindup || t > TUNE.swingWindup + win) return;
|
||||
@@ -1712,7 +1720,7 @@ export function update(dt) {
|
||||
G.aiReactT += dt;
|
||||
const incoming = ball.active && ball.lastHit === 'P' && G.aiReactT >= TUNE.aiReaction;
|
||||
const tgt = incoming ? intercept('O') : HOME.O;
|
||||
const spd = TUNE.aiSpeed * (incoming ? 1 : 0.5); // slow recovery: wrong-footing wins points
|
||||
const spd = TUNE.aiSpeed * (G.diff ? G.diff.speed : 1) * (incoming ? 1 : 0.5); // slow recovery: wrong-footing wins points
|
||||
const dx = tgt.x - O.x, dz = tgt.z - O.z, d = Math.hypot(dx, dz);
|
||||
if (d > 0.1) {
|
||||
O.x += dx / d * Math.min(spd * dt, d);
|
||||
@@ -1731,11 +1739,13 @@ export function update(dt) {
|
||||
// CLASSIC TENNIS turns the band OFF and flubs less: the band exists to get a new player
|
||||
// PAST the opener, and a mode whose whole promise is a real match cannot be won by an
|
||||
// opponent that falls apart the moment he goes ahead.
|
||||
const flub = G.classic ? TUNE.classicFlub : TUNE.aiFlub + score.games[1] * TUNE.aiFlubLoss;
|
||||
const flub = G.classic ? (G.diff ? G.diff.flub : TUNE.classicFlub)
|
||||
: TUNE.aiFlub + score.games[1] * TUNE.aiFlubLoss;
|
||||
if (Math.random() < flub) G.aiSwingCool = 0.5; // whiff: hesitates and misses
|
||||
else {
|
||||
O.swingT = TUNE.swingWindup + TUNE.swingWindow / 2;
|
||||
trySwing(O, true, G.classic ? TUNE.classicQualityMax : TUNE.aiQualityMax); O.swingT = -1;
|
||||
trySwing(O, true, G.classic ? (G.diff ? G.diff.quality : TUNE.classicQualityMax)
|
||||
: TUNE.aiQualityMax); O.swingT = -1;
|
||||
G.aiSwingCool = ball.lastHit === 'O' ? 0.4 : 0.12;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user