From 911035af00943535311e29e7e2c7a098df20f4fb Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Sat, 8 Aug 2026 21:10:11 +0200 Subject: [PATCH] The card lies flat, and a popup owns its screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - icon LEFT of the text: every card is 62px at a fixed top, fully over the crowd — the stacked layout hung its lower third over live court, and the art draws bigger this way, not smaller - nothing draws or ticks under a modal: a card raised in the same beat used to burn out dimmed beneath it — it holds and plays after Co-Authored-By: Claude Fable 5 --- HANDOFF.md | 9 +++++++++ gameplay.js | 14 ++++++++------ index.html | 2 +- render.js | 40 +++++++++++++++++++++------------------- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index a903f0a..c8ea542 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -442,6 +442,15 @@ the countdown panel ('CHANGEOVER · RESUMING IN n'). He kept the burst-arm weapo praise words. **A bottom/above-rack message slot was proposed and REJECTED**: that is thumb territory — covered by the hand mid-pull and outside where the eyes are. +**Fourth batch, same day — the card went HORIZONTAL, and nothing draws under a modal.** The +stacked icon card ran 102 px tall and its lower third hung over live court; the icon sits LEFT of +the text now (boxed to 58×52 so a wide tin cannot reach the type — and it draws BIGGER than the +stacked layout gave it), so every card is a flat `CARD_H` 62 px at fixed `CARD_TOP` 96, fully over +the crowd. `bandTop()` is gone with it. And a modal owns the whole screen now: the flash and the +card are neither DRAWN under its dim nor TICKED behind the freeze (the card tick moved below the +modal early-return, next to the flash's) — a card raised in the same beat as a popup used to burn +its whole life dimmed-out underneath it, and now it holds and plays after the dismissal. + **Second batch, same day — THE MESSAGE BAND.** Oliver: transient messages need "a definite space … over the crowd". `AD_TOP()`/`bandTop(h)` in render.js pin the whole family there — below the scoreboard block, above the ad strip, never on the court: diff --git a/gameplay.js b/gameplay.js index 779ec5a..21f7085 100644 --- a/gameplay.js +++ b/gameplay.js @@ -1326,12 +1326,6 @@ export function update(dt) { updateZombies(dt); return; } - // A card can wait before it shows. The chapter card used to land on the same frame as the boss - // died, so the biggest gore burst in the game happened underneath it and nobody ever saw it. - if (G.card) { - if (G.card.wait > 0) G.card.wait -= dt; - else { G.card.t -= dt; if (G.card.t <= 0) G.card = null; } - } if (G.shake && (G.shake.t -= dt) <= 0) G.shake = null; if (G.netRipple && (G.netRipple.t -= dt) <= 0) G.netRipple = null; // the tape settles if (G.zoom && (G.zoom.t += dt) >= G.zoom.T) G.zoom = null; // scripted punch-in (render applies it) @@ -1351,6 +1345,14 @@ export function update(dt) { // late in a run — the weapon modal is first-sight-only, so once you have seen a tin the label // flash IS the announcement, and the trainer's popup was eating it every time you were wounded. if (G.flash) { G.flash.t -= dt; if (G.flash.t <= 0) G.flash = null; } + // A card can wait before it shows. The chapter card used to land on the same frame as the boss + // died, so the biggest gore burst in the game happened underneath it and nobody ever saw it. + // BELOW the modal return, like the flash: a card raised in the same beat as a popup used to + // burn its whole life dimmed-out underneath it — now it holds and plays AFTER the dismissal. + if (G.card) { + if (G.card.wait > 0) G.card.wait -= dt; + else { G.card.t -= dt; if (G.card.t <= 0) G.card = null; } + } for (let i = pops.length - 1; i >= 0; i--) if ((pops[i].t -= dt) <= 0) pops.splice(i, 1); // reaction pops decay crowd.cheer = Math.max(0, crowd.cheer - dt); G.idleT += dt; diff --git a/index.html b/index.html index 7df0e1b..388343c 100644 --- a/index.html +++ b/index.html @@ -359,6 +359,6 @@ - + diff --git a/render.js b/render.js index 08dfbc1..bc0dd78 100644 --- a/render.js +++ b/render.js @@ -1178,30 +1178,29 @@ 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 -// 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); +// The band has ROWS: the flash line owns 68..90 (FLASH_Y, Bangers 18), the countdown panel the +// strip under it, and the card a FIXED slot below both. The card is ONE compact height now, icon +// or not — the icon sits LEFT of the text instead of stacking above it. Stacked, an icon card ran +// 102 px tall and its lower third hung over live court; side-by-side it is 62 px flat, sits fully +// over the crowd, and the art actually draws BIGGER than the stacked layout gave it. +const CARD_TOP = 96, CARD_H = 62; 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 - // rather than the art crowding the type. - const img = icon && SPR[icon], ih = img ? 34 : 0; - const y0 = bandTop(68 + ih); - ctx.fillStyle = 'rgba(10,6,8,0.72)'; ctx.fillRect(0, y0, W, 68 + ih); - ctx.textAlign = 'center'; - if (img) { // the tin itself, at its own aspect, sat on the title - const w = Math.round(img.width * (ih - 6) / img.height); + const img = icon && SPR[icon]; + ctx.fillStyle = 'rgba(10,6,8,0.72)'; ctx.fillRect(0, CARD_TOP, W, CARD_H); + if (img) { // left of the text block, vertically centred, boxed so a wide tin can't reach the type + const k = Math.min((CARD_H - 10) / img.height, 58 / img.width); + const iw = Math.round(img.width * k), ih = Math.round(img.height * k); ctx.imageSmoothingEnabled = false; - ctx.drawImage(img, Math.round(W / 2 - w / 2), y0 + 3, w, ih - 6); + ctx.drawImage(img, 8, Math.round(CARD_TOP + (CARD_H - ih) / 2), iw, ih); } // the title still SHOUTS (Bangers) but at 22, not 30 — up here it shares the screen with the // scoreboard and the ads, and 30 read as oversized the moment it left centre court - ctx.font = B(22); shadowText(title, W / 2, y0 + ih + 8, color); + ctx.textAlign = 'center'; + ctx.font = B(22); shadowText(title, W / 2, CARD_TOP + 4, color); ctx.font = F(8); - if (sub1) shadowText(sub1, W / 2, y0 + ih + 36, UI.cream); - if (sub2) shadowText(sub2, W / 2, y0 + ih + 50, UI.cream); + if (sub1) shadowText(sub1, W / 2, CARD_TOP + 34, UI.cream); + if (sub2) shadowText(sub2, W / 2, CARD_TOP + 48, UI.cream); } // A GHOST HAND, fingertip at (x, y) — the tutorial finger every gesture hint shares. A drawn @@ -1825,7 +1824,10 @@ export function render() { // The free-floating register: no panel, one fixed slot, shouted. PERFECT, GAME N, DOUBLE KILL, // RETURNED, CHANGEOVER, weapon labels — everything that used to be plain white 12px text sitting // directly on the crowd. It keeps its 1px jitter; the shadow is what makes it legible anywhere. - if (G.flash) { + // ...and neither the flash nor the card draws UNDER a modal: the popup owns the screen, and a + // band message half-visible through its dim was clutter saying nothing. Their clocks are frozen + // behind the freeze too (see update), so both play out in full once the popup is dismissed. + if (G.flash && !G.modal) { // 18, not 24: at the card's near-size a transient one-liner read as a milestone announcement, // and half of what lands in this slot (PERFECT, RETURNED, DOUBLE KILL) is only ever a footnote // to something you already watched happen. The register is right; the volume was not. @@ -1833,7 +1835,7 @@ export function render() { shadowText(G.flash.text, W / 2, FLASH_Y + Math.sin(G.flash.t * 20) * 1, '#ffffff'); } // the SET milestone card (the duel's own announcement is a real modal now, not a timed card) - if (G.card && G.card.t > 0 && !(G.card.wait > 0)) // `wait` holds it OFF the screen (a boss dying under it) + if (G.card && !G.modal && G.card.t > 0 && !(G.card.wait > 0)) // `wait` holds it OFF the screen (a boss dying under it) drawCard(G.card.title, G.card.sub1, G.card.sub2, G.card.color, G.card.icon); if (TUNE.missHud && G.lastMiss && G.idleT - G.lastMiss.at < 3.5) { // last miss reason, live ctx.font = F(8); ctx.fillStyle = '#ff9060';