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.
  • ArtifactBanner shape: id, name, icon, tierColor, life, age, smoothed displayY, and needsSpawnSeed flag.
  • 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-uiScale pixels).
  • The three exported public functions: pushArtifactBanner, clearArtifactBanners, drawArtifactBanners.

READS FROM

  • uiScale, W from ../core/state for scaled geometry and horizontal clamping against screen width.
  • ARTIFACT_MAP and ARTIFACT_TIER_COLOR_BY_IDX from ../../data/artifacts for name, icon, and tier color.
  • getArtifactTier from ../world/artifacts to look up the tier index (clamped 0..4) for color selection.
  • getTopArtifactSlotPos from ./hud for the anchor position (top edge of the topmost artifact icon).
  • medFontBody from ./medical-canvas-palette for body-text typography (Space Grotesk with fallback).

PUSHES TO

  • The supplied CanvasRenderingContext2D only — fills, strokes, text, and globalAlpha changes wrapped in ctx.save() / ctx.restore().
  • Internal mutable state: appends to _banners (with overflow trimming to MAX_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-frame dt for 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 in EffectEngine.
  • 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 — clearArtifactBanners is 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 within BANNER_THROTTLE of the previous push for the same artifact, or if the artifact id is unknown to ARTIFACT_MAP.
  • clearArtifactBanners(): inbound reset — drops all banners and clears throttle history.
  • drawArtifactBanners(ctx, dt, _shipRef): per-frame tick + render. The _shipRef parameter is accepted but unused (underscore-prefixed); kept for caller signature stability.

Entry points

  • pushArtifactBanner — called from EffectEngine on artifact effect fire.
  • drawArtifactBanners — called once per frame from bridge.ts.
  • clearArtifactBanners — called at run start and run end.

Pattern notes

  • Stack ordering: unshift puts 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 to displayY → targetY. At 60fps with rate=14 this is approximately 0.21, matching the legacy dt * 14 feel, but stays consistent in wall-clock time at 120fps or under load.
  • Spawn seeding uses a needsSpawnSeed boolean rather than an age < dt epsilon check — the first frame after spawn places displayY at targetY - 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 between pad and W - w - pad so 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 into ARTIFACT_TIER_COLOR_BY_IDX (C/B/A/S/Mythic identity). Flagged in source with V32-RARITY-COLOR as the canonical tier identity routing.
  • All geometry constants are pre-uiScale and 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) so splice during iteration is safe.