Exact touchdown, the boy finishes his shift, healing announces itself, 4K heads

Five of Oliver's six (the sixth is a play-mode discussion):
- THE DOUBLE FAULTS WERE FRAME QUANTIZATION: the bounce registered a full
  integration step after true contact — ~0.75 m of travel at serve pace — so
  serves aimed half a metre safe were judged long. Classic enforces the
  service box, the match only the court, hence 'zombie tennis does not'.
  The bounce now rewinds to y=0 before anything reads the position.
  Measured in-pane: 30/40 faults on good taps before, 0/40 after (mistimed
  and smudged taps also 0/40; aimed hard serves keep their scatter risk).
  Also: the strike flick dead zone is 44 px so a smudged tap stays a tap,
  and the aim base clamps 0.18 m inside the lines — scatter, not the aim,
  is what faults.
- The mop beat holds the walk-on until the boy actually LEAVES the court
  (ordered off at beat end, 6 s cap).
- A match game win that restores blood shows the duel's WOUNDS TREATED
  popup; full-blood wins stay silent.
- The opponent walk-on card holds 4.0 s (was 2.4).
- Card heads are cut from the 4K sheets at 144 px (assets/_heads.py,
  z_<fam>head.png, HEAD_CROP stays as fallback) — the old 65 px crops were
  the 'very low res' heads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-13 21:09:04 +02:00
1 parent c847bcbfa1
commit c6218a6284
17 files changed
+178 -23

No files matched your search

+28 -8
View File
@@ -417,11 +417,22 @@ export function tickMatchFall(dt) {
// on with its own intro card, exactly as it used to on the kill frame.
const d = G.match;
if (d && d.mopT > 0 && (d.mopT -= dt) <= 0) {
d.mopT = 0;
matchOpponent();
d.ending = false; // play re-opens only now that a body is on court
matchReady();
G.phase = 'over'; G.overT = 2.2; // matchReady's setupPoint re-arms pretoss; keep the old beat
// Time is up — but THE COURT IS HIS UNTIL HE LEAVES IT (Oliver: "no playing as long as the
// mopboy is on court"). He is ordered off (jobs zeroed, the half-pushed stain dropped) and
// the walk-on holds until he is clear of the playing area; the 6 s cap is a belt on top of
// the braces — nothing should ever need it, but a stalled beat must not stall the match.
const b = G.boy;
if (b && b.mop && Math.abs(b.x) < 6.5 && (d.mopHold = (d.mopHold || 0) + dt) < 6) {
b.jobs = 0; b.tx = undefined; b.tz = undefined; b.mopT = 0;
if (G.interlude && G.interlude.introT < 1) G.interlude.introT = 1; // serving stays barred
d.mopT = 0.001; // the beat re-checks next tick
} else {
d.mopT = 0; d.mopHold = 0;
matchOpponent();
d.ending = false; // play re-opens only now that a body is on court
matchReady();
G.phase = 'over'; G.overT = 2.2; // matchReady's setupPoint re-arms pretoss; keep the old beat
}
}
if (!G.matchFall) return tickMatchServe(dt);
const f = G.matchFall;
@@ -506,8 +517,10 @@ function matchOpponent() {
// so killZombie can file the kill under it without asking G which opponent we were on.
zb.foe = matchFoe(G.match.opp);
zb.fam = zb.foe.fam; // ...and the body it wears (config.ZFAMS)
// the walk-on card wears the opponent's own head (config.HEAD_CROP — a crop, not new art)
G.card = { title: zb.foe.name, sub1: zb.foe.line, color: UI.hot, t: 2.4, head: zb.fam };
// the walk-on card wears the opponent's own head (config.HEAD_CROP — a crop, not new art).
// 4.0 s: at 2.4 the head + name + punchline were gone before they were read ("the intro card
// of the player should last longer")
G.card = { title: zb.foe.name, sub1: zb.foe.line, color: UI.hot, t: 4.0, head: zb.fam };
zb.hp = zb.hpMax = TUNE.matchOppHp; // a FLAT pool: with regicide gone the body is pure
// scoreboard (a quarter per point), so a deeper pool would
// change nothing — the ladder escalates in racketAI instead
@@ -574,7 +587,14 @@ function matchGameWon(zb, banked = false) {
// on a game point) — bank the game by hand or the scoreboard drifts from the ladder.
if (!banked) { score.games[0]++; score.pts = [0, 0]; }
G.runGames++; // the ladder metric, same as every mode
G.hp = Math.min(G.hpMax, G.hp + TUNE.matchHeal); // a clean game is how you last
// a clean game is how you last — and when it actually restores blood, the trainer SAYS so
// (Oliver: "show wounds healed popup in zombie tennis when appropriate"). Same popup as the
// duel's, so healing has one face everywhere; a full-blood win stays silent.
const healed = Math.min(G.hpMax, G.hp + TUNE.matchHeal) - G.hp;
G.hp += healed;
if (healed > 0)
showModal({ title: 'WOUNDS TREATED', icon: 'x_cross', iconH: 52, color: UI.pick,
body: ['THE TRAINER PATCHED YOU UP', G.hp + ' OF ' + G.hpMax + ' BACK'] });
voice('game_player', 0.3); cheer(2.4, 0.6);
// GAME is the FLASH and the next opponent's name plate is the CARD. It used to be the other way
// round and matchOpponent's plate landed on the same slot a frame later, so 'GAME.' never drew