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

+17 -7
View File
@@ -221,7 +221,7 @@ function farB(z) {
// Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the
// menu screens hang <img> tags on the same files, and two copies of this number is one copy too
// many: the one that does not get bumped is the one that serves a cached 404.
export const SPRV = 111;
export const SPRV = 112;
// The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was
// already requested at import — it just makes the wait visible and holds the menu back until the
// last sheet has landed. See the onerror note below for the other half of "sometimes missing".
@@ -273,6 +273,9 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
// filenames typed out here (each of which is a chance to typo one and have a body
// vanish for a frame — the oldest bug in this file).
...ZFAMS.flatMap(famFrames),
// the card's 4K-cut head icons (assets/_heads.py). 'r' has no fam sheet — his
// card keeps the HEAD_CROP fallback, so no file and no 404 for him.
...ZFAMS.filter(k => k !== 'r').map(k => 'z_' + k + 'head'),
'w_ball','w_bomb','w_triple','w_fire','w_gun', // weapon-rack icons
'w_beacon','w_laser','w_rocket','w_flamer', // ...and the four that come out of a barrel
'z_barrel1','z_barrel2', // the weapon barrel, two frames so it ROLLS
@@ -1711,10 +1714,16 @@ function cardBody(c, dx) {
// and boss names of any length, and Bangers 22 ran a long one off both edges of a phone. Measure
// once and shrink to the band; 22 stays 22 for everything that already fit.
// THE OPPONENT CARD CARRIES ITS HEAD (Oliver: the punchline plus a picture of his head). The
// icon is a straight crop out of the family's own healthy frame — config.HEAD_CROP, measured,
// never separate art — drawn left of the text block, which shifts right to keep the whole
// group centred as one. A family with no crop row degrades to the plain text card.
const hd = c.head && HEAD_CROP[c.head] && SPR['z_' + c.head + '1'] ? HEAD_CROP[c.head] : null;
// icon prefers the 4K-cut z_<fam>head.png (assets/_heads.py — the installed 224 px frames gave
// ~65 px heads drawn at 144 device px, which is the "very low res" he reported) and degrades
// to the HEAD_CROP crop of the healthy frame, then to the plain text card. Never separate art:
// both sources are the body already on court.
const headImg = c.head && SPR['z_' + c.head + 'head'];
const hd = headImg ? { img: headImg, w: headImg.width, h: headImg.height }
: c.head && HEAD_CROP[c.head] && SPR['z_' + c.head + '1']
? { img: SPR['z_' + c.head + '1'], box: HEAD_CROP[c.head],
w: HEAD_CROP[c.head].w, h: HEAD_CROP[c.head].h }
: null;
const ih = hd ? CARD_H - 14 : 0, iw = hd ? Math.round(hd.w * ih / hd.h) : 0;
const room = W - 24 - (hd ? iw + 8 : 0);
ctx.font = B(22);
@@ -1731,8 +1740,9 @@ function cardBody(c, dx) {
let blockW = 0;
for (const [t, f] of lines) { ctx.font = f; blockW = Math.max(blockW, ctx.measureText(t).width); }
ctx.imageSmoothingEnabled = false;
ctx.drawImage(SPR['z_' + c.head + '1'], hd.x, hd.y, hd.w, hd.h,
Math.round(cx - blockW / 2 - 8 - iw), Math.round(CARD_MID - ih / 2), iw, ih);
const hx = Math.round(cx - blockW / 2 - 8 - iw), hy = Math.round(CARD_MID - ih / 2);
if (hd.box) ctx.drawImage(hd.img, hd.box.x, hd.box.y, hd.box.w, hd.box.h, hx, hy, iw, ih);
else ctx.drawImage(hd.img, hx, hy, iw, ih);
}
for (const [t, f, rel, col] of lines) { ctx.font = f; shadowText(t, cx, y + rel, col); }
ctx.textAlign = 'center'; ctx.restore(); // the band's neighbours (flash, countdown) assume centred