The staff learn your habits, the walk-on waits for your tap, and the club opens a practice court

Four answers to one phone report:
- THE READ: repeat a lane and the opponent anticipates it; break the habit
  and it is wrong-footed for that ball — scaled by rung, so surprise is the
  weapon that opens the ladder past the HEAD COACH (TUNE block THE READ).
- The opponent walk-on is a real modal now (head, punchline, tap to dismiss)
  in arcade, tournament and training — readable without a clock.
- THE TRAINING COURT: third card under ZOMBIE TENNIS, pick any rung, one
  game at a time, the felled body walks straight back on, nothing banks,
  bleeding out is a popup instead of a death. mode_train.png rolled in the
  banner style.
- MOVEMENT READS: sprinting feet kick per-court dust and fast bodies leave
  a two-ghost speed streak (trailMin gates both; feel verdict pending).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-28 16:08:18 +02:00
1 parent a42dc6fc81
commit 43784c5b33
9 files changed
+360 -43

No files matched your search

+26 -3
View File
@@ -1,7 +1,7 @@
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
import { cv, ctx } from './dom.js';
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS, HEAD_CROP, CUP_ROUNDS, TINT, SHOT_DEPTH } from './config.js';
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS, HEAD_CROP, CUP_ROUNDS, TINT, SHOT_DEPTH, matchFoe } from './config.js';
import { P, O, ball, score, crowd, G, court, tour, kit, skin } from './state.js';
import { RAISERS, FRAMES } from './crowd.js';
import { predictLanding, previewServe, slotId, slotStock, serveApexY, rallyFeel, swayOf } from './gameplay.js';
@@ -805,9 +805,21 @@ function drawMachine(zb) {
}
const p0 = zb => proj(zb.x, 0, zb.z).s; // projected scale, for effects that need pixels not metres
function drawZombie(zb) {
function drawZombie(zb, ghost) {
if (zb.variant === 'o') return drawBarrel(zb);
if (zb.variant === 'm') return drawMachine(zb);
// MOVEMENT READS: a body genuinely sprinting (only racketAI writes vx/vz — the shamble never
// crosses trailMin) leaves two ghosts sampled back along its own velocity, the modal masthead's
// speed streak applied to a body. Ghosts draw first so the live sprite lands on top, and never
// during a named frame — a collapsing corpse must not smear on stale velocity.
if (!ghost && TUNE.trailMin && !zb.showFrame
&& Math.hypot(zb.vx || 0, zb.vz || 0) > TUNE.trailMin) {
for (const [k, a] of [[2, 0.10], [1, 0.22]]) {
ctx.save(); ctx.globalAlpha *= a;
drawZombie({ ...zb, x: zb.x - zb.vx * TUNE.trailK * k, z: zb.z - (zb.vz || 0) * TUNE.trailK * k }, true);
ctx.restore();
}
}
const rk = zb.variant === 'r';
// THE MATCH OPPONENT HAS ITS OWN SPRITES NOW. It used to borrow the turned CPU's ('c') — a body
// drawn to SHAMBLE at the camera with its racket hanging at its side, which is what was playing
@@ -1782,10 +1794,19 @@ export function drawLook(cv2, t, k, sk) {
const bounceDx = () => tour.dx ?? BOUNCE_IDLE.dx;
const bounceApex = () => tour.apex ?? BOUNCE_IDLE.apex;
function drawChar(ch) {
function drawChar(ch, ghost) {
// No split-step. The receiver used to be slid 0.12 m side to side while waiting on a serve, on top
// of a swap to the run frames; both are gone. Sliding a sprite sideways is not footwork — it reads
// as the man skating — and the idle pair's own weight shift is the whole of what he should be doing.
// MOVEMENT READS: the same two-ghost speed streak the bodies wear (see drawZombie) — gameplay's
// mover writes P.vx/vz, so only a genuine sprint (auto-run chase, horde steer) ever draws one.
if (!ghost && TUNE.trailMin && Math.hypot(ch.vx || 0, ch.vz || 0) > TUNE.trailMin) {
for (const [k, a] of [[2, 0.10], [1, 0.22]]) {
ctx.save(); ctx.globalAlpha *= a;
drawChar({ ...ch, x: ch.x - ch.vx * TUNE.trailK * k, z: ch.z - ch.vz * TUNE.trailK * k }, true);
ctx.restore();
}
}
const p = proj(ch.x, 0, ch.z);
// the readability boost is what makes the MATCH OPPONENT legible: at the far baseline he was
// ~34 logical px of faithful anatomy, which is mush. The near player anchors the curve (farB
@@ -2822,6 +2843,8 @@ export function render() {
// the cup names the ROUND — its opp is a drawn rung, so the deathmatch arithmetic lied here
ctx.fillText(G.match && G.match.cup
? `${CUP_ROUNDS[G.match.cup.round].name} · ${G.match.cup.round} FELLED`
// training: the ladder is parked, so "opponent N · felled" would lie — name the rep instead
: G.match && G.match.train ? `TRAINING · ${matchFoe(G.match.opp).short}`
: G.match ? `OPPONENT ${G.match.opp} · ${G.match.opp - 1} FELLED`
: G.classic ? `SET ${G.sets[0] + G.sets[1] + 1} · FIRST TO ${TUNE.classicSets}`
: `GAME ${G.runGames + 1} · ${G.sets[0]} SET${G.sets[0] === 1 ? '' : 'S'}`, bW - 3, bH + 8);