PURPOSE
Maintains and renders a rolling stack of “[Artifact] activated” banners on the side of the screen, anchored to the top of the artifact icon column. Banners are pushed when an artifact effect with showBanner=true fires, throttled per-artifact, capped in count, slide in from above at spawn, fade out at end of life, and the surviving stack lerps up to fill vacated slots.
OWNS
- Module-scope mutable banner stack
_banners(newest at index 0, older cascading downward) and the per-artifact throttle map_lastPushTime. ArtifactBannershape: id, name, icon, tierColor, life, age, smootheddisplayY, andneedsSpawnSeedflag.- Banner timing constants: throttle, life, fade-in, fade-out, max visible, per-stack-position opacity.
- Banner geometry constants: width, height, gap, slide-in offset, ease rate, edge padding (all in pre-
uiScalepixels). - The three exported public functions:
pushArtifactBanner,clearArtifactBanners,drawArtifactBanners.
READS FROM
uiScale,Wfrom../core/statefor scaled geometry and horizontal clamping against screen width.ARTIFACT_MAPandARTIFACT_TIER_COLOR_BY_IDXfrom../../data/artifactsfor name, icon, and tier color.getArtifactTierfrom../world/artifactsto look up the tier index (clamped 0..4) for color selection.getTopArtifactSlotPosfrom./hudfor the anchor position (top edge of the topmost artifact icon).medFontBodyfrom./medical-canvas-palettefor body-text typography (Space Grotesk with fallback).
PUSHES TO
- The supplied
CanvasRenderingContext2Donly — fills, strokes, text, and globalAlpha changes wrapped inctx.save()/ctx.restore(). - Internal mutable state: appends to
_banners(with overflow trimming toMAX_VISIBLE), writes to_lastPushTime, decays and splices banners during draw.
DOES NOT
- Does not subscribe to events or own its own timer — relies on the caller’s time source (
game.time) for throttle correctness across pause/level-up screens, and on the per-framedtfor life decay and easing. - Does not allocate or own off-screen surfaces — draws directly into the passed canvas context.
- Does not look up artifact effects, listen to the effect engine, or filter by
showBanner— that gating lives upstream inEffectEngine. - Does not handle input, layout adjacent HUD elements, or know about anything below the topmost artifact slot beyond the cy/r it receives.
- Does not survive a run —
clearArtifactBannersis called at run start and run end to wipe both the stack and the throttle map.
Signals
pushArtifactBanner(artifactId, nowSec): inbound queue point — fired by the effect engine when an artifact effect with banner flag activates. Silently drops if withinBANNER_THROTTLEof the previous push for the same artifact, or if the artifact id is unknown toARTIFACT_MAP.clearArtifactBanners(): inbound reset — drops all banners and clears throttle history.drawArtifactBanners(ctx, dt, _shipRef): per-frame tick + render. The_shipRefparameter is accepted but unused (underscore-prefixed); kept for caller signature stability.
Entry points
pushArtifactBanner— called fromEffectEngineon artifact effect fire.drawArtifactBanners— called once per frame frombridge.ts.clearArtifactBanners— called at run start and run end.
Pattern notes
- Stack ordering:
unshiftputs newest at index 0; overflow trims the tail (_banners.length = MAX_VISIBLE) so the most recent activation always survives. - Frame-rate-independent easing:
ease = 1 - exp(-BANNER_EASE_RATE * dt)applied todisplayY → targetY. At 60fps withrate=14this is approximately 0.21, matching the legacydt * 14feel, but stays consistent in wall-clock time at 120fps or under load. - Spawn seeding uses a
needsSpawnSeedboolean rather than anage < dtepsilon check — the first frame after spawn placesdisplayYattargetY - BANNER_SLIDE_IN_OFFSET * uiScale(above the slot) and clears the flag; the lerp takes over on the next frame to slide the banner downward into place. - Alpha pipeline: base alpha comes from
STACK_OPACITY[i](1.0 / 0.7 / 0.4 for top / middle / bottom slots), multiplied by fade-in ramp (age / BANNER_FADE_IN) and fade-out ramp (life / BANNER_FADE_OUT). Zero-alpha banners short-circuit the draw. - Horizontal placement: centered on the anchor’s
cx, then clamped betweenpadandW - w - padso banners stay off safe-area edges on narrow phones. - Drawing order inside
_drawOne: dark panel rectangle, top + bottom tier-colored 2px lines, tier-tinted icon disc with emoji glyph centered, then two-segment text — uppercase name in tier color followed by ” ACTIVATED” in white, each with a one-pixel-down black drop shadow drawn first. - Tier color path:
getArtifactTier(id)→ clamped 0..4 → indexed intoARTIFACT_TIER_COLOR_BY_IDX(C/B/A/S/Mythic identity). Flagged in source withV32-RARITY-COLORas the canonical tier identity routing. - All geometry constants are pre-
uiScaleand multiplied at draw time, keeping the design-time numbers readable in source while honoring the device-scaled canvas. - Decay loop runs back-to-front (
for i = length-1 .. 0) sospliceduring iteration is safe.