The band became one axis: fitted plates, and cards that travel
Every transient message — milestone card, RESUMING clock, one-line flash — now centres its ink on CARD_MID and only one is ever up. Three shared pieces do all of it: inkSpan measures, plate() sizes the fill to what it holds, slideDraw swipes it in from the left and off to the right with a four-ghost smear sampled back along its own easing curve. - weapon card is icon + name + count, one centred row, no prose - plain cards centre as a block instead of hanging off the band's top edge - the clock left the net for the band; it takes over when a card or flash clears - band moved up 20px into the flash's old row, which it can now that they never draw together - hold times +0.8s: the swipe eats ~0.55s that the old numbers didn't budget for Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8658a93dc7
commit
f352612de3
3 files changed
+144
-52
No files matched your search
@@ -184,7 +184,9 @@ export const TUNE = {
|
|||||||
zPushback: 1.4, // m a staggered zombie is knocked back
|
zPushback: 1.4, // m a staggered zombie is knocked back
|
||||||
splatterOverT: 0.1, // s the spent-ball 'over' beat lasts before the next serve is armed
|
splatterOverT: 0.1, // s the spent-ball 'over' beat lasts before the next serve is armed
|
||||||
stuckT: 6, // s the deadlock watchdog waits before letting go of a frozen latch (see update)
|
stuckT: 6, // s the deadlock watchdog waits before letting go of a frozen latch (see update)
|
||||||
tinCardT: 2.0, // s the won-duel weapon card holds (it does NOT wait for a tap)
|
tinCardT: 2.8, // s the won-duel weapon card holds (it does NOT wait for a tap). The swipe-in
|
||||||
|
// and swipe-out eat ~0.55 s of this, so every card number below is REAL time on
|
||||||
|
// screen minus that — 2.0 left the old static hold, and read as a flicker.
|
||||||
// the slingshot: the pull's DIRECTION picks a lane, its LENGTH is power. Deliberately DECOUPLED —
|
// the slingshot: the pull's DIRECTION picks a lane, its LENGTH is power. Deliberately DECOUPLED —
|
||||||
// reaching a far shambler must not cost a longer drag than the screen has room for. The old
|
// reaching a far shambler must not cost a longer drag than the screen has room for. The old
|
||||||
// mapping (pull length = reach) is why you had to drag off the bottom of the phone.
|
// mapping (pull length = reach) is why you had to drag off the bottom of the phone.
|
||||||
@@ -342,8 +344,8 @@ export const TUNE = {
|
|||||||
burnBallR: 2.4, // m — the flame ball also lights anything it FLIES PAST. Without this the ball's
|
burnBallR: 2.4, // m — the flame ball also lights anything it FLIES PAST. Without this the ball's
|
||||||
// full pierce simply killed the whole lane outright and nothing was left to catch fire.
|
// full pierce simply killed the whole lane outright and nothing was left to catch fire.
|
||||||
burnPanic: 1.0, // walk-speed multiplier while burning (1 = off; crank it for panicked sprinting)
|
burnPanic: 1.0, // walk-speed multiplier while burning (1 = off; crank it for panicked sprinting)
|
||||||
setCardT: 2.0, // s the centered SET card holds (the 'kurze unterbrechung' before the horde)
|
setCardT: 2.8, // s the centered SET card holds (the 'kurze unterbrechung' before the horde)
|
||||||
bossWinT: 3.6, // ...and s the EPIC! card holds after a chapter boss. Its OWN number: a boss is
|
bossWinT: 4.4, // ...and s the EPIC! card holds after a chapter boss. Its OWN number: a boss is
|
||||||
// the longest fight in the game and the win was over before it registered, but
|
// the longest fight in the game and the win was over before it registered, but
|
||||||
// stretching setCardT would have made every routine set announcement drag too.
|
// stretching setCardT would have made every routine set announcement drag too.
|
||||||
modalIn: 0.34, // s the modal swooshes in — icon+title from the right, body cascading from the
|
modalIn: 0.34, // s the modal swooshes in — icon+title from the right, body cascading from the
|
||||||
|
|||||||
+4
-6
@@ -630,12 +630,10 @@ export function deliverTin() {
|
|||||||
if (wasPlain) G.weapon = 0;
|
if (wasPlain) G.weapon = 0;
|
||||||
sendBoy(); voice('new_balls', 0.6);
|
sendBoy(); voice('new_balls', 0.6);
|
||||||
aside('ump_new_balls_dark', 2.0); // "...and a new opponent, apparently."
|
aside('ump_new_balls_dark', 2.0); // "...and a new opponent, apparently."
|
||||||
// THE PRIZE GETS ITS OWN CARD, with the tin's own icon on it — the announcement AND the whole
|
// THE PRIZE GETS ITS OWN CARD: the tin's picture, its name, and how many. Nothing else — the
|
||||||
// explanation. The first-sight weapon MODAL is gone (with popBarrel's): the testteam never read
|
// first-sight MODAL is gone (a popup that freezes a live court mid-run costs runs) and so is the
|
||||||
// them, and a popup that freezes a live court mid-run costs runs. The card carries the one desc
|
// prose that replaced it. You have just won a point; you want to know what landed and how much.
|
||||||
// line that matters and does not want a tap: you have just won a point, not been asked a question.
|
G.card = { title: WEAPONS[id].label, sub1: '×' + WEAPONS[id].tin,
|
||||||
G.card = { title: WEAPONS[id].label, sub1: 'DELIVERED TO THE RACK',
|
|
||||||
sub2: (WEAPONS[id].desc && WEAPONS[id].desc[0]) || '',
|
|
||||||
icon: WEAPONS[id].icon, color: UI.warm, t: TUNE.tinCardT };
|
icon: WEAPONS[id].icon, color: UI.warm, t: TUNE.tinCardT };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1181,40 +1181,122 @@ function drawRack() {
|
|||||||
// the centered milestone 'block': a real pause, front and center. Shared by the sudden-death duel
|
// the centered milestone 'block': a real pause, front and center. Shared by the sudden-death duel
|
||||||
// and the SET announcement so both read as the same interruption. Assumes textAlign is already set.
|
// and the SET announcement so both read as the same interruption. Assumes textAlign is already set.
|
||||||
// ---- THE MESSAGE BAND. Every transient centred message — the milestone card, the RESUMING
|
// ---- THE MESSAGE BAND. Every transient centred message — the milestone card, the RESUMING
|
||||||
// countdown, the one-line flash (FLASH_Y already lived here) — has ONE definite home: over the
|
// countdown, the one-line flash — has ONE definite home: over the CROWD, below the scoreboard
|
||||||
// CROWD, below the scoreboard block, above the ad strip. Nothing transient sits on the court
|
// block, above the ad strip. Nothing transient sits on the court anymore — a card at centre court
|
||||||
// anymore — a card at centre court covered the very play it was announcing, non-blocking or not.
|
// 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: the flash line owns 68..90 (FLASH_Y, Bangers 18), the countdown panel the
|
// The band no longer has ROWS — it has an AXIS. Card, countdown and flash all centre their ink on
|
||||||
// strip under it, and the card a FIXED slot below both. The card is ONE compact height now, icon
|
// CARD_MID; only one of the three is ever up (see cardUp/flashUp), so they cannot collide, and the
|
||||||
// or not — the icon sits LEFT of the text instead of stacking above it. Stacked, an icon card ran
|
// eye learns one place to look instead of three stacked slots. The icon sits LEFT of the text
|
||||||
// 102 px tall and its lower third hung over live court; side-by-side it is 62 px flat, sits fully
|
// rather than above it: stacked, an icon card ran 102 px tall and its lower third hung over live
|
||||||
// over the crowd, and the art actually draws BIGGER than the stacked layout gave it.
|
// court; side-by-side it is flat, sits fully over the crowd, and the art draws BIGGER than the
|
||||||
const CARD_TOP = 88, CARD_H = 62, CARD_PAD = 20; // px of side padding inside the band; top sits
|
// stacked layout gave it. CARD_H is the MAXIMUM plate height — the plate itself is sized to what
|
||||||
// flush under the flash row (ends ~88)
|
// it actually holds (see plate()), so a one-line countdown gets a slim strip instead of a
|
||||||
|
// three-line box with nothing in two thirds of it.
|
||||||
|
const CARD_TOP = 68, CARD_H = 62, CARD_PADY = 9; // px of breathing room above and below the ink
|
||||||
|
const CARD_MID = CARD_TOP + CARD_H / 2; // the axis everything in the band sits on
|
||||||
|
|
||||||
function drawCard(title, sub1, sub2, color, icon) {
|
// THE CARD IS A PHYSICAL THING that gets carried on and carried off, courtside-sign style: it
|
||||||
|
// swipes in from the left, holds, and leaves to the right. Entry overshoots and settles back
|
||||||
|
// (easeOutBack) so it arrives with weight; the exit accelerates away (easeInCubic) so it reads as
|
||||||
|
// leaving rather than fading. The whole card moves, plate included — a sign travels, its backing
|
||||||
|
// does not stay bolted across the screen while the type slides out from under it.
|
||||||
|
const CARD_IN = 0.44, CARD_OUT = 0.24; // seconds; clamped below so a short card still gets both.
|
||||||
|
// The entry is the SLOWER of the two on purpose: at 0.3 the overshoot covered the whole width in
|
||||||
|
// about four frames, so the smear happened off-screen and only the exit ever showed a trail.
|
||||||
|
// x offset for something that lives for T0 seconds and has t left. Anything with a countdown can
|
||||||
|
// ride the band this way — the card does, and so does the RESUMING clock.
|
||||||
|
function slideX(T0, t) {
|
||||||
|
const tIn = Math.min(CARD_IN, T0 / 3), tOut = Math.min(CARD_OUT, T0 / 3);
|
||||||
|
const el = T0 - t;
|
||||||
|
if (el < tIn) { const k = Math.max(0, el) / tIn - 1, s = 1 + 2.7 * k * k * k + 1.7 * k * k; return -W * (1 - s); }
|
||||||
|
if (t < tOut) { const k = 1 - t / tOut; return W * k * k * k; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MOTION BLUR: ghost copies sampled BACK IN TIME along the card's own easing curve, so the smear
|
||||||
|
// stretches exactly as far as the card actually travelled — long through the entry snap, short as
|
||||||
|
// it settles, and nothing at all while it holds. Sampling the curve beats differencing frames:
|
||||||
|
// it looks identical at 60 fps and at the calm-state 20, and needs no per-card state.
|
||||||
|
const CARD_GHOSTS = 4, CARD_GHOST_DT = 0.026; // 4 copies over ~100 ms of history — the trail length
|
||||||
|
// The RESUMING clock's own T0, latched on the frame it takes the band and cleared when it leaves.
|
||||||
|
// A card has a fixed life to slide against; the clock's is whatever was left when the band freed up.
|
||||||
|
let cdT0 = 0;
|
||||||
|
function slideDraw(T0, t, body) {
|
||||||
|
const dx = slideX(T0, t);
|
||||||
|
for (let i = CARD_GHOSTS; i >= 1; i--) {
|
||||||
|
const gx = slideX(T0, t + i * CARD_GHOST_DT);
|
||||||
|
if (Math.abs(gx - dx) < 1.5) continue; // holding: no trail to draw
|
||||||
|
ctx.globalAlpha = 0.42 / i;
|
||||||
|
body(gx);
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
body(dx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCard(c) {
|
||||||
|
const T0 = c.T0 || (c.T0 = c.t); // recorded on the first drawn frame — `wait` holds
|
||||||
|
slideDraw(T0, c.t, dx => cardBody(c, dx)); // `t` frozen until then, so this is exact
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardBody(c, dx) {
|
||||||
|
const { title, sub1, sub2, color, icon } = c;
|
||||||
const img = icon && SPR[icon];
|
const img = icon && SPR[icon];
|
||||||
ctx.fillStyle = 'rgba(10,6,8,0.72)'; ctx.fillRect(0, CARD_TOP, W, CARD_H);
|
ctx.save(); ctx.translate(Math.round(dx), 0);
|
||||||
// TWO layouts, by whether there is art: a plain card (SET, CHAPTER, the intros) stays fully
|
// TWO layouts, by whether there is art. The WEAPON card is one centred row — art, name, amount,
|
||||||
// CENTRED, the classic announcement; an icon card goes ragged-left beside its inset art —
|
// measured as a single group and centred as one. No prose: the tin's own picture says what it is
|
||||||
// centred type next to a left icon read as two unrelated things sharing a band.
|
// and the count says how much, and the sentence explaining it was a line nobody read mid-run.
|
||||||
let tx = W / 2; ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
if (img) { // vertically centred, boxed small — the art is a bullet point, not the message
|
if (img) {
|
||||||
const k = Math.min(36 / img.height, 38 / img.width);
|
const k = Math.min(44 / img.height, 46 / img.width);
|
||||||
const iw = Math.round(img.width * k), ih = Math.round(img.height * k);
|
const iw = Math.round(img.width * k), ih = Math.round(img.height * k);
|
||||||
|
ctx.font = B(26); const nameW = ctx.measureText(title).width;
|
||||||
|
ctx.font = B(20); const amtW = sub1 ? 8 + ctx.measureText(sub1).width : 0;
|
||||||
|
let x = Math.round((W - (iw + 10 + nameW + amtW)) / 2);
|
||||||
|
const s = inkSpan([[title, B(26), 0]]);
|
||||||
|
plate(Math.max(ih, s.bot - s.top)); // the art is usually the taller of the two
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
ctx.drawImage(img, CARD_PAD, Math.round(CARD_TOP + (CARD_H - ih) / 2), iw, ih);
|
ctx.drawImage(img, x, Math.round(CARD_MID - ih / 2), iw, ih);
|
||||||
tx = CARD_PAD + iw + 12; ctx.textAlign = 'left';
|
ctx.textAlign = 'left'; x += iw + 10;
|
||||||
|
ctx.font = B(26); shadowText(title, x, inkCentre([[title, B(26), 0]]), color);
|
||||||
|
if (sub1) { ctx.font = B(20); shadowText(sub1, x + nameW + 8, inkCentre([[sub1, B(20), 0]]), UI.cream); }
|
||||||
|
ctx.textAlign = 'center'; ctx.restore();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// 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. The stack
|
||||||
ctx.font = B(22); shadowText(title, tx, CARD_TOP + 4, color);
|
// keeps its own line rhythm (0/30/44) and is centred as ONE block, so a one-line card and a
|
||||||
ctx.font = F(8);
|
// three-line card sit on the same axis instead of both hanging off the band's top edge.
|
||||||
if (sub1) shadowText(sub1, tx, CARD_TOP + 34, UI.cream);
|
const lines = [[title, B(22), 0, color]];
|
||||||
if (sub2) shadowText(sub2, tx, CARD_TOP + 48, UI.cream);
|
if (sub1) lines.push([sub1, F(8), 30, UI.cream]);
|
||||||
ctx.textAlign = 'center'; // the band's neighbours (flash, countdown) assume centred
|
if (sub2) lines.push([sub2, F(8), 44, UI.cream]);
|
||||||
|
const s = inkSpan(lines);
|
||||||
|
plate(s.bot - s.top);
|
||||||
|
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); }
|
||||||
|
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:
|
||||||
|
// the three-line SET card is what that number was tuned for and it still fills it exactly.
|
||||||
|
function plate(h) {
|
||||||
|
const ph = Math.min(CARD_H, Math.round(h) + CARD_PADY * 2);
|
||||||
|
ctx.fillStyle = 'rgba(10,6,8,0.72)';
|
||||||
|
ctx.fillRect(0, Math.round(CARD_MID - ph / 2), W, ph);
|
||||||
|
}
|
||||||
|
// The measured INK extent of a stack of [text, font, relativeY]. Off ink, not off the font box:
|
||||||
|
// Bangers carries a lot of leading, so a nominal 'middle' baseline (or a fixed offset tuned on one
|
||||||
|
// string) hangs the caps visibly high. textBaseline is 'top' here, so ascent is negative — ink runs
|
||||||
|
// rel-ascent .. rel+descent. Everything in the band sizes and centres itself off this.
|
||||||
|
function inkSpan(lines) {
|
||||||
|
let top = Infinity, bot = -Infinity;
|
||||||
|
for (const [t, f, rel] of lines) {
|
||||||
|
ctx.font = f; const m = ctx.measureText(t);
|
||||||
|
top = Math.min(top, rel - m.actualBoundingBoxAscent);
|
||||||
|
bot = Math.max(bot, rel + m.actualBoundingBoxDescent);
|
||||||
|
}
|
||||||
|
return { top, bot };
|
||||||
|
}
|
||||||
|
function inkCentre(lines) { const s = inkSpan(lines); return Math.round(CARD_MID - (s.top + s.bot) / 2); }
|
||||||
|
|
||||||
// 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
|
||||||
// pointing hand rather than the old abstract dot: testers did not read a sliding circle as "your
|
// pointing hand rather than the old abstract dot: testers did not read a sliding circle as "your
|
||||||
@@ -1416,9 +1498,7 @@ function drawModal(m) {
|
|||||||
// ------------------------------------------------------------------ frame ----
|
// ------------------------------------------------------------------ frame ----
|
||||||
const ballHist = []; // hot-ball ghost trail positions (render-owned, see the streak block in render)
|
const ballHist = []; // hot-ball ghost trail positions (render-owned, see the streak block in render)
|
||||||
const AD_H = 14; // px tall the courtside ad strip is drawn — also the ceiling for centred HUD text
|
const AD_H = 14; // px tall the courtside ad strip is drawn — also the ceiling for centred HUD text
|
||||||
const FLASH_Y = 68; // the ONE slot every free-floating message uses. Clear of the scoreboard block
|
// (FLASH_Y is gone — the flash centres on CARD_MID with everything else in the band now.)
|
||||||
// (6 + 42 + 15 = 63 at its tallest), which the old 60 was not in portrait —
|
|
||||||
// there the board is over half the width, so a centred flash landed on it.
|
|
||||||
export function render() {
|
export function render() {
|
||||||
ctx.setTransform(RES, 0, 0, RES, 0, 0); // logical W x H coordinates at RES density
|
ctx.setTransform(RES, 0, 0, RES, 0, 0); // logical W x H coordinates at RES density
|
||||||
// ease the aim camera FIRST, before anything projects — one camera per frame (see proj)
|
// ease the aim camera FIRST, before anything projects — one camera per frame (see proj)
|
||||||
@@ -1785,16 +1865,6 @@ export function render() {
|
|||||||
: Math.max(G.changeover > 0 ? G.changeover : 0,
|
: Math.max(G.changeover > 0 ? G.changeover : 0,
|
||||||
G.phase === 'over' ? G.overT : 0,
|
G.phase === 'over' ? G.overT : 0,
|
||||||
G.interlude ? G.interlude.introT : 0);
|
G.interlude ? G.interlude.introT : 0);
|
||||||
if (waitT > 0.5) {
|
|
||||||
// OVER THE NET, in the shout face, no box — Oliver's call after trying it panelled in the
|
|
||||||
// band: the countdown belongs to the COURT the pause is holding, and Bangers + shadowText is
|
|
||||||
// the register that survives any backdrop (it is what every flash already rides).
|
|
||||||
// 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);
|
|
||||||
ctx.font = B(16);
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
shadowText(txt, W / 2, Math.round(proj(0, COURT.netH, COURT.netZ).y) - 26, '#ffffff');
|
|
||||||
}
|
|
||||||
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde && !(G.mgT > 0) && !G.aim && waitT <= 0.5) { // while pulling, the draw meter owns this slot
|
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde && !(G.mgT > 0) && !G.aim && waitT <= 0.5) { // while pulling, the draw meter owns this slot
|
||||||
ctx.font = F(8);
|
ctx.font = F(8);
|
||||||
const touch = IS_TOUCH, switchHint = touch ? 'TAP THE RACK TO SWITCH' : '1-0 TO SWITCH';
|
const touch = IS_TOUCH, switchHint = touch ? 'TAP THE RACK TO SWITCH' : '1-0 TO SWITCH';
|
||||||
@@ -1833,16 +1903,38 @@ export function render() {
|
|||||||
// ...and neither the flash nor the card draws UNDER a modal: the popup owns the screen, and a
|
// ...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
|
// 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.
|
// behind the freeze too (see update), so both play out in full once the popup is dismissed.
|
||||||
if (G.flash && !G.modal) {
|
// ONE MESSAGE AT A TIME. A flash stacked above a card was two announcements shouting over each
|
||||||
|
// other in the same band; the CARD wins because it is the bigger event (a set, a chapter, a
|
||||||
|
// weapon won) and the flash under it is always a footnote to something you already watched.
|
||||||
|
// The flash's clock keeps running, so it expires quietly rather than surfacing late.
|
||||||
|
const cardUp = G.card && !G.modal && G.card.t > 0 && !(G.card.wait > 0);
|
||||||
|
const flashUp = G.flash && !G.modal && !cardUp;
|
||||||
|
if (flashUp) {
|
||||||
// 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.
|
||||||
|
// On the band's axis, not on its top edge: the flash is the only band citizen with no plate,
|
||||||
|
// so hanging it where a plate's first line would start left it sitting visibly high with
|
||||||
|
// nothing under it. Same ink-centring every card uses — one axis for all three registers.
|
||||||
ctx.font = B(18);
|
ctx.font = B(18);
|
||||||
shadowText(G.flash.text, W / 2, FLASH_Y + Math.sin(G.flash.t * 20) * 1, '#ffffff');
|
shadowText(G.flash.text, W / 2, inkCentre([[G.flash.text, B(18), 0]]) + 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.modal && G.card.t > 0 && !(G.card.wait > 0)) // `wait` holds it OFF the screen (a boss dying under it)
|
if (cardUp) drawCard(G.card); // `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);
|
// THE RESUMING CLOCK is a band citizen too — same plate, same swipe, same smear. It used to
|
||||||
|
// float bare over the net in its own face and position, which was the last message in the game
|
||||||
|
// that looked like it came from a different game. It is the LOWEST priority of the three: a card
|
||||||
|
// outranks it (the card explains the very pause it is counting) and so does a flash (short, and
|
||||||
|
// always about something that just happened). It swipes in the moment the band frees up.
|
||||||
|
// The 0.5 gate is on STARTING only — once it is on the band it plays down to zero, or the
|
||||||
|
// swipe-out (0.24 s) would be cut off before it ever ran.
|
||||||
|
if (waitT > 0 && (cdT0 || waitT > 0.5) && !cardUp && !flashUp) {
|
||||||
|
cdT0 = cdT0 || waitT; // its own T0: the clock left when the band came free
|
||||||
|
const n = Math.ceil(waitT);
|
||||||
|
slideDraw(cdT0, waitT, dx => cardBody(
|
||||||
|
G.changeover > 0 ? { title: 'CHANGEOVER', sub1: 'RESUMING IN ' + n, color: '#ffffff' }
|
||||||
|
: { title: 'RESUMING IN ' + n, color: '#ffffff' }, dx));
|
||||||
|
} else cdT0 = 0;
|
||||||
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';
|
||||||
ctx.fillText(G.lastMiss.text.toUpperCase(), W / 2, H - 28);
|
ctx.fillText(G.lastMiss.text.toUpperCase(), W / 2, H - 28);
|
||||||
|
|||||||
Reference in new issue
Block a user