The card lies flat, and a popup owns its screen

- 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 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-08 21:10:11 +02:00
1 parent 1337048a85
commit 911035af00
4 files changed
+39 -26

No files matched your search

+9
View File
@@ -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 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. 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 **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 … 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: scoreboard block, above the ad strip, never on the court:
+8 -6
View File
@@ -1326,12 +1326,6 @@ export function update(dt) {
updateZombies(dt); updateZombies(dt);
return; 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.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.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) 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 // 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. // 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; } 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 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); crowd.cheer = Math.max(0, crowd.cheer - dt);
G.idleT += dt; G.idleT += dt;
+1 -1
View File
@@ -359,6 +359,6 @@
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="main.js?v=131"></script> <script type="module" src="main.js?v=132"></script>
</body> </body>
</html> </html>
+21 -19
View File
@@ -1178,30 +1178,29 @@ function drawRack() {
// CROWD, below the scoreboard block, above the ad strip. Nothing transient sits on the court // 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. // 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 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 band has ROWS: the flash line owns 68..90 (FLASH_Y, Bangers 18), the countdown panel the
// the strip under it, so a card may never climb above 96 — a tall icon card used to reach the // strip under it, and the card a FIXED slot below both. The card is ONE compact height now, icon
// flash line and the two printed over each other, which was the "spamming over each other" report. // or not — the icon sits LEFT of the text instead of stacking above it. Stacked, an icon card ran
// The floor wins over the ad-strip anchor: a too-tall card overlaps the ads (decoration) instead. // 102 px tall and its lower third hung over live court; side-by-side it is 62 px flat, sits fully
const bandTop = h => Math.max(96, AD_TOP() - h - 4); // 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) { 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 const img = icon && SPR[icon];
// rather than the art crowding the type. ctx.fillStyle = 'rgba(10,6,8,0.72)'; ctx.fillRect(0, CARD_TOP, W, CARD_H);
const img = icon && SPR[icon], ih = img ? 34 : 0; if (img) { // left of the text block, vertically centred, boxed so a wide tin can't reach the type
const y0 = bandTop(68 + ih); const k = Math.min((CARD_H - 10) / img.height, 58 / img.width);
ctx.fillStyle = 'rgba(10,6,8,0.72)'; ctx.fillRect(0, y0, W, 68 + ih); const iw = Math.round(img.width * k), ih = Math.round(img.height * k);
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);
ctx.imageSmoothingEnabled = false; 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 // 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 // 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); ctx.font = F(8);
if (sub1) shadowText(sub1, W / 2, y0 + ih + 36, UI.cream); if (sub1) shadowText(sub1, W / 2, CARD_TOP + 34, UI.cream);
if (sub2) shadowText(sub2, W / 2, y0 + ih + 50, 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 // 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, // 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 // 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. // 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, // 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 // 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. // 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'); 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) // 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); 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 if (TUNE.missHud && G.lastMiss && G.idleT - G.lastMiss.at < 3.5) { // last miss reason, live
ctx.font = F(8); ctx.fillStyle = '#ff9060'; ctx.font = F(8); ctx.fillStyle = '#ff9060';