Serve reads, the mop beat, the head-icon card, and Daniel benched
Oliver's batch, the code half: - The tennis serve lost its ghost fingers (idle tap demo + apex rider deleted with their counters; the rally-return hint survives). - The toss rises over the tossing HAND, measured off the toss frames' raised fingertips (TOSS.dx/oDx, women's tour override) — not over the head. - An uncaught toss returns to the bounce stance instead of flashing the smash frame the player never earned. - Between match games the court belongs to the mop boy: 5 s (TUNE.matchMopT), full job allowance, a hurry speed between stains, and the far-court reach limit lifted for the match (his stains are exactly where opponents die). The next opponent walks on only after — d.ending guards the empty court. - The walk-on card wears the opponent's own head: config.HEAD_CROP, straight crops measured off the family sheets, no new art. - The match intro is DEATH MATCH, counting the real blood pool. - Daniel is benched: every ump_* personality line routes through aside()'s early return; officiating calls keep speaking. Personality pass later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e01894f4d
commit
c084d53497
8 files changed
+141
-70
No files matched your search
+29
-40
@@ -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 } 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 } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, court, tour } from './state.js';
|
||||
import { predictLanding, previewServe, slotId, slotStock, serveApexY } from './gameplay.js';
|
||||
import { drillQueue, drillLegal, drillMult } from './drill.js';
|
||||
@@ -1708,16 +1708,31 @@ function cardBody(c, dx) {
|
||||
// ...and the title FITS. Cards carry whole sentences now ("SWING TO RETURN — OR SWISH IT AWAY")
|
||||
// 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;
|
||||
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);
|
||||
const tw = ctx.measureText(title).width;
|
||||
const tSize = tw > W - 24 ? Math.max(13, Math.floor(22 * (W - 24) / tw)) : 22;
|
||||
const tSize = tw > room ? Math.max(13, Math.floor(22 * room / tw)) : 22;
|
||||
const lines = [[title, B(tSize), 0, color]];
|
||||
if (sub1) lines.push([sub1, F(8), 30, UI.cream]);
|
||||
if (sub2) lines.push([sub2, F(8), 44, UI.cream]);
|
||||
const s = inkSpan(lines);
|
||||
plate(s.bot - s.top);
|
||||
plate(Math.max(s.bot - s.top, ih));
|
||||
const y = Math.round(CARD_MID - (s.top + s.bot) / 2);
|
||||
for (const [t, f, rel, col] of lines) { ctx.font = f; shadowText(t, W / 2, y + rel, col); }
|
||||
const cx = Math.round(W / 2 + (hd ? (iw + 8) / 2 : 0));
|
||||
if (hd) {
|
||||
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);
|
||||
}
|
||||
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
|
||||
}
|
||||
// The plate, sized to the `h` px of content it carries and centred on the axis. Capped at CARD_H:
|
||||
@@ -1841,37 +1856,10 @@ function drawPullHint() {
|
||||
demoSwipe(dir, 0, 'SWIPE TO DODGE');
|
||||
}
|
||||
|
||||
// ---- the TENNIS lessons, same ghost hand. Tennis has no slingshot to mime, but the testteam
|
||||
// struggled to grasp the tap ritual too — so the hand TAPS: on the apron when the player sits idle
|
||||
// in the serve stance, riding the toss when they keep dropping it, and beside the player when
|
||||
// returns keep sailing past untouched. Each appears on the exact struggle it answers and goes
|
||||
// away the moment the player does the thing (the counters live in state.G, reset in gameplay).
|
||||
function drawTapHint() {
|
||||
const p0 = proj(P.x, 0, P.z);
|
||||
const ax = Math.round(p0.x) + 20, ay = Math.round(p0.y) + 14; // beside his feet, on the apron
|
||||
const t = (G.idleHintT - TUNE.pullHintAfter) % 1.6; // hover · press · hold · lift
|
||||
const press = t < 0.6 ? 0 : t < 0.75 ? (t - 0.6) / 0.15 : t < 0.95 ? 1 : Math.max(0, 1 - (t - 0.95) / 0.2);
|
||||
ctx.save(); ctx.globalAlpha = 0.9;
|
||||
if (t >= 0.72 && t < 1.32) drawTapRing(ax, ay, (t - 0.72) / 0.6);
|
||||
drawGhostHand(ax, ay, press);
|
||||
ctx.font = F(10); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP TO TOSS', ax - 20, ay + 40, UI.warm);
|
||||
shadowText('TAP AGAIN AT THE TOP', ax - 20, ay + 54, UI.warm);
|
||||
ctx.restore();
|
||||
}
|
||||
// two dropped tosses in a row: the hand rides the ball itself and presses exactly at the apex,
|
||||
// so the TIMING is the thing being shown, not just the verb
|
||||
function drawApexHint() {
|
||||
const b = proj(ball.x, ball.y, ball.z);
|
||||
const apex = TOSS.y0 + TOSS.vy * TOSS.vy / (2 * TUNE.gravity);
|
||||
const press = Math.max(0, 1 - Math.abs(ball.y - apex) / 0.45);
|
||||
ctx.save(); ctx.globalAlpha = 0.9;
|
||||
if (press > 0.55) drawTapRing(b.x + 4, b.y + 4, 1 - press); // tight at the apex, blooming as it passes
|
||||
drawGhostHand(b.x + 4, b.y + 4, press);
|
||||
ctx.font = F(10); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP AT THE TOP', b.x, b.y - 28, UI.warm);
|
||||
ctx.restore();
|
||||
}
|
||||
// ---- the TENNIS lesson, same ghost hand. The serve hints (an idle tap demo on the apron, an
|
||||
// apex rider on a dropped toss) lived here too and were DELETED on Oliver's report — a ghost
|
||||
// finger over the running serve animation read as clutter, and the zombie duel serves fine with
|
||||
// no teacher at all. Only the return lesson below survives.
|
||||
// two returns passed untouched: the hand presses harder as the ball closes on the player
|
||||
function drawHitHint() {
|
||||
const p0 = proj(P.x, 0, P.z);
|
||||
@@ -2423,7 +2411,11 @@ export function render() {
|
||||
// after a beat of inaction the apron demo takes over, and it says PULL DOWN in its own words —
|
||||
// so this slot shuts up rather than printing the same instruction a second time 40 px below it
|
||||
const demo = G.mode === 'splatter' && !G.interlude && !gunLike && G.idleHintT > TUNE.pullHintAfter;
|
||||
const tapDemo = G.mode === 'tennis' && G.serveNum !== 2 && G.idleHintT > TUNE.pullHintAfter;
|
||||
// NO GHOST HAND ON THE TENNIS SERVE — not the idle tap demo, not the apex rider. Oliver: "this
|
||||
// ghost finger over the running serve animation is distracting. this must be removed. it is
|
||||
// not in zombie tennis mode" — and the duel serving fine without a teacher is the argument.
|
||||
// The rally-return hint (drawHitHint) stays: it answers a different struggle and he did not
|
||||
// flag it. drawTapHint/drawApexHint were deleted with their counters.
|
||||
// The gesture-teaching strings ('PULL DOWN TO AIM…', 'TAP TO TOSS…') are GONE — the ghost hand
|
||||
// is the teacher now, and saying the same thing twice in two registers was clutter. This slot
|
||||
// only carries STATE the mime cannot: which weapon the serve fires, and a second serve.
|
||||
@@ -2435,11 +2427,8 @@ export function render() {
|
||||
: '';
|
||||
if (txt) shadowText(txt, W / 2, METER_Y(), UI.warm); // same slot the draw meter uses
|
||||
if (demo) drawPullHint(); // show the gesture instead of only naming it
|
||||
if (tapDemo) drawTapHint();
|
||||
}
|
||||
// the tennis struggle hints: each rides the exact failure it answers (see drawTapHint's block)
|
||||
if (G.mode === 'tennis' && score.server === 'P' && G.phase === 'toss' && G.tossDrops >= 2 && ball.active)
|
||||
drawApexHint();
|
||||
// the one surviving tennis struggle hint: rides the exact failure it answers
|
||||
if (G.mode === 'tennis' && G.phase === 'rally' && G.passStreak >= 2 && ball.active && ball.lastHit === 'O')
|
||||
drawHitHint();
|
||||
// The free-floating register: no panel, one fixed slot, shouted. PERFECT, GAME N, DOUBLE KILL,
|
||||
|
||||
Reference in new issue
Block a user