The reaction lands where the ball does

- praise was firing at RELEASE, half a second before impact (measured:
  32-frame flight, word on frame 0) — it now arms on the ball and pops
  on the first body it meets, or its first bounce
- DOUBLE KILL / MASSACRE / RETURNED rise off the thing that earned them,
  as world-anchored pop texts — the announcement band is for milestones
- the band got rows: a card can no longer print over the flash line
- GAME N flash deleted (the scoreboard bar already says it), CHANGEOVER
  folded into the countdown panel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-08 20:50:46 +02:00
1 parent 9476f354c2
commit 1337048a85
7 files changed
+91 -30

No files matched your search

+20 -3
View File
@@ -4,7 +4,7 @@ import { cv, ctx } from './dom.js';
import { TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_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 { P, O, ball, score, crowd, G, court, tour } from './state.js';
import { predictLanding, previewServe, slotId, slotStock } from './gameplay.js';
import { zombies, gore, piles, parts, pellets, extras, shots } from './zombies.js';
import { zombies, gore, piles, parts, pellets, extras, shots, pops } from './zombies.js';
// ---------------------------------------------------------------- palette ----
// tennis palette -> drained splatter palette; the transition lerps between them.
@@ -1178,7 +1178,11 @@ function drawRack() {
// CROWD, below the scoreboard block, above the ad strip. Nothing transient sits on the court
// anymore — a card at centre court covered the very play it was announcing, non-blocking or not.
const AD_TOP = () => Math.round(proj(0, 0, -6).y) - AD_H; // top of the courtside ad strip
const bandTop = h => Math.max(56, AD_TOP() - h - 4); // fit an h-px band above the ads
// The band has ROWS now: the flash line owns 68..90 (FLASH_Y, Bangers 18) and the countdown panel
// the strip under it, so a card may never climb above 96 — a tall icon card used to reach the
// flash line and the two printed over each other, which was the "spamming over each other" report.
// The floor wins over the ad-strip anchor: a too-tall card overlaps the ads (decoration) instead.
const bandTop = h => Math.max(96, AD_TOP() - h - 4);
function drawCard(title, sub1, sub2, color, icon) {
// An ICON (the won-duel tin) needs its own room above the title, so the band grows for it
@@ -1646,6 +1650,18 @@ export function render() {
ctx.beginPath(); ctx.ellipse(p.x, p.y, p.s * 0.62 * k, p.s * 0.22 * k, 0, 0, 7); ctx.stroke();
}
if (G.mode !== 'eaten') drawChar(P); // ...the dying man is drawn with the crowd instead (drawDying)
// world-anchored reaction pops (zombies.popText): DOUBLE KILL rises off the corpse, the praise
// word lands on the impact. Drawn IN the world pass, before the un-shake, so they ride the kick
// of the moment they belong to. Small, brief, and where you are already looking — the reactions
// left the announcement band because up there they read as one more banner shouting over play.
for (const p of pops) {
const f = p.t / p.T, pr = proj(p.x, p.y, p.z);
ctx.save();
ctx.globalAlpha = Math.min(1, f * 3); // holds, then a quick fade at the end
ctx.font = B(13); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
shadowText(p.text, Math.round(pr.x), Math.round(pr.y - (1 - f) * 14), p.col || '#ffffff');
ctx.restore();
}
// (a PERFECT-contact spark ring lived here for one build — Oliver's verdict: too much. The
// hitstop, the click and the praise word already carry the moment.)
// The braced "rambo racket" is NOT drawn any more. It was a procedural racket laid over a sprite
@@ -1762,7 +1778,8 @@ export function render() {
// In the message band with the rest of the family, and on its OWN PANEL: bare shadowed text
// was barely readable over the sunlit clay crowd (Oliver flagged it every court since).
// Plain Silkscreen on purpose — a countdown is information, not an announcement.
const txt = 'RESUMING IN ' + Math.ceil(waitT);
// the changeover names itself here — its old separate flash was the same word, twice
const txt = (G.changeover > 0 ? 'CHANGEOVER · ' : '') + 'RESUMING IN ' + Math.ceil(waitT);
const y = Math.min(FLASH_Y + 26, AD_TOP() - 18);
ctx.font = F(10);
const tw = Math.ceil(ctx.measureText(txt).width);