PURPOSE

Canvas-2D HUD render — every overlay drawn on top of the world each frame. Covers the XP bar, HP and shield bars, weapon slots with WoW-style GCD overlay and per-weapon fire flash, modifier and artifact slot columns, mission timer, killstreak, FPS readout, heat bar, boss bar tween, warnings, directional hit chevrons, portal and supply-pod arrows, cog and help buttons, tutorial overlays, magnet flash, low-HP pulse, and the lantern fill meter. Also hosts the reward-cards pipeline (level-up cards, upgrade-show, merge cinematic, banish targeting) and the death cinematic and damage-flash overlays. Hard perf contract is enforced in the file header: drawHUD() must complete in < 2ms, no ctx.clip(), no per-frame gradients, no shadowBlur, no globalCompositeOperation changes outside the listed weapon-slot glow paths, font set at most three or four times per frame, and ctx.arc() only inside the weapon-slot GCD ring (bounded < 8 arcs/frame).

OWNS

  • drawHUD(ctx) — single main entry point. Bails out when game.phase === 'menu' or !ship.alive or when the HUD band is under 100 CSS px wide.
  • HP and shield premium-bar layout: helpers _drawPremiumBar, _drawIconBadge, _slantedRect. Constants barH = 18 * uiScale, barR = 6 * uiScale, iconR = 14 * uiScale, SLANT = 0.08, badge overlap, two equal columns under the XP bar.
  • HP/shield gain-flash state: _prevHpMax, _prevShieldMax, _hpGainFlash, _shieldGainFlash, _hpGainFrac, _shieldGainFrac. Flash fires when ship.hpMax or ship.shieldMax increases versus the previous frame, decays at 2.5×/s, paints the newly-gained right-edge fraction white.
  • XP bar block: vampire-survivors-style full-width flush-to-top bar. xpBarH = 22 * uiScale, multi-stop lime/aqua-jade xpGrad plus vertical jade-edge overlay, centered “Level X” text in Cal Sans medical head 700 with stacked dark stroke, and a white whiteout _xpFlash triggered on game.xp increase, decayed at 4×/s. Reads XP_THRESHOLDS[game.level] and [game.level + 1].
  • Weapon slot block (weaponSlotCount = game.weaponSlotsMax || 4): dynamic-sized slot circles (_dMaxDiam = 65 * uiScale, _dMinGap = 6 * uiScale, _dAvailW = (_hudR - _hudL) - m * 2, gap up to 10 * uiScale), wpnBaseY = H - m - wR - 18 * uiScale. Per-slot: dark #080f1a background disc, optional ready golden glow (#fbbf24, three 'lighter' ring strokes, only when w.fireTimer <= 0.02), rarity-colored border ring from RARITY_HEX, weapon emoji fallback WEAPON_EMOJIS[w.id] || '?', WoW-style yellow GCD arc draining clockwise from top (-π/2 → cdPct * 2π), ctx.filter = 'saturate(0)' on icon while on cooldown, weapon PNG icon via getWeaponIconImg(w.id), per-weapon juice on _fireFlash > 0 (recoil/squeeze/shake/jitter patterns keyed by w.id for rifle, revolver, shotgun, railgun, missile, lightning, cannon, mortar, flame, disc, sweep, line, barrier, generic fallback), level badge bottom-right, manual-mode 'M' badge top-left, fire-flash ring pulse colored by damage tag (#3bb4ff energy / #ff3b3b explosion / #ff8a1c fire / #ffffff bullet), burnout #222222 gray + red X when ship.stalled, weapon name label under the slot ('EMPTY' for empty slots), and damage-tag pills under the name via drawDamageTagPill. Empty slots render a dim #444455 outline.
  • Modifier slots column (top-right): modR = 14 * uiScale, vertical stack of only FILLED slots (no empty circles, infinite count) from game.upgradeCounts. Dark navy disc, icon via getWeaponIconImg('mod_' + id) or getOrBakeEmojiIcon(modDef.icon), level badge bottom-right.
  • Artifact slots column (top-right, one column left of modifiers): artR = 18 * uiScale. Per-artifact pulse stroke (tierColor from ARTIFACT_TIER_COLOR_BY_IDX, 2.5 Hz sin pulse phased by slot index), artifact-color inner glow ring (def.colors.primary), emoji icon, tier-letter badge ['C','B','A','S'][inst.tier], and an activation pop animation driven by getArtifactFlash(inst.id) — scale overshoots to ~1.5× with white overlay disc, bright outer ring (def.colors.bright || .primary, 4 × uiScale width), and a thinner inner primary ring.
  • Boss bar: full helper _drawBossBar(ctx) returns the Y offset for the XP bar. Owns the slide-in / fade-out tween (_bossBarT driven by game._rawDt so it plays through pause and slow-mo), smoothed HP (_bossBarSmoothHp eases at ~12/s toward the live sum, drains at ~6/s during fade-out), _bossBarFlash damage pulse (eased over BOSS_BAR_FLASH_DUR = 0.25s), constants BOSS_BAR_SLIDE_IN = 0.4, BOSS_BAR_FADE_OUT = 0.6, BOSS_BAR_HEIGHT = 28 pre-uiScale. Renders semi-transparent black background, colored fill (def.barColor), white flash overlay, hairline border, uppercase name with 3-line stacked shadow, and right-aligned hp / hpMax numerals. getBossBar(worldState) is exported and computes the bar pool from every alive enemy with sharesHealthWithBoss; bar identity comes from BOSS_DEFS[game._activeBossDefId].
  • Mission timer block (top-right, big Cal Sans head 600): countdown when !game.overtime; under 30 s pulses MedicalPalette.statusBad; in overtime, switches to count-up OT M:SS flashing between MedicalPalette.magenta and MedicalPalette.statusBad (6 Hz), with up to 20 skull glyphs () stacked left of the timer one per 15 s of overtime. Below the timer, level indicator Lv.N in MedicalPalette.accentBright cyan when game._currentLevel > 1.
  • FPS counter under the bars top-left, color from MedicalPalette.statusGood (≥50) / statusWarn (≥30) / statusBad (<30), small medical body font at 0.7 alpha.
  • Killstreak label under the FPS counter when game.killStreak >= 5. Color and text from STREAK_MILESTONES[game.lastStreakMilestone] or fallback ${streak}× STREAK.
  • Heat bar (left side, vertically centered): tapered “speedometer” shape with sine-wavy outline (WAVE_COUNT = 6, WAVE_AMP = 2 * uiScale, slow 0.25 Hz drift). widthTop = 22 * uiScale, widthBot = 14 * uiScale, heatH = H * 0.33. Fill is a nebula gradient (yellow → orange → red → purple → blue → white-hot top) driven by ship.heat / 100. Star Power swaps in a pastel rainbow hue-cycle driven by game.time * 240. Danger flash (red glow + stroke) at ≥ 80 % heat (suppressed by Star Power), pulse frequency lerps 2 → 22 Hz as heat climbs. MPH readout above the bar (speedMph = round(hypot(ship.vx, ship.vy) * 100), shows nK over 1000, color #ff9944 over 9000). Fire emoji below the bar while playerInput.isThrusting && !starPowerActive, pulsing red 2 → 12 Hz once hP >= 0.80.
  • Cog (bottom-left) + help ? (bottom-right) buttons. Cog uses the gear UI sprite with damped-sine squash/stretch over COG_PRESS_DUR = 0.28 s. Help disc is a low-alpha medical chrome circle with a centered white ?. Both buttons set their hit boxes (_cogHitBox, _helpHitBox) each frame.
  • Warnings system: shared queue warnings: HudWarning[] with text/color/glow/life/duration/priority/bg?/stroke?, per-text warningCooldowns (3 s suppression), pulse and fade-in/fade-out animation, and combat-urgency variant (bold Cal Sans, jumbo size for 'STALLED', optional color stroke) vs modern variant (white fill + stacked black 3D shadow, Space Grotesk 700) via _drawText3DShadow. Warnings are cleared on game.phase === 'levelup' or any active reward state. 'OVERHEATING' / 'STALLED' use the 6 Hz red pulse.
  • Directional hit indicators: ring buffer _hitIndicators (max 6), HIT_INDICATOR_DURATION = 0.8. Hits within ~30° of an existing indicator refresh in place. Rendered as bright red #ff2222 rectangles at min(W, H) * 0.42 from screen center, with a #ff8888 inner core.
  • Portal indicator _drawPortalIndicator: amber #dd88ff chevron at the screen edge always pointing toward game.portal while the portal exists and isn’t 'entered', plus an additional bobbing downward chevron above the portal when on-screen. Pulse 0.6 + 0.4 * sin(game.time * 4).
  • Supply-pod arrows _drawSupplyPodArrows: iterates getPropPool().forEachEdgeArrowProp(...) and renders amber #ffcc66 edge chevrons / on-screen bobbing pointers, with a “dramatic” hot-pulse modulation for pods more than DRAMA_DIST_PX = 900 away.
  • Magnet flash icon: black-body / red-tip magnet symbol with white radial-gradient glow, driven by game._magnetIconTimer over a 1.5 s lifetime with 18 Hz flash.
  • Lantern fill meter (_lanternFill, _lanternVisible, _lanternAlpha): retained as a back-compat hook; setLanternFill is now a no-op (@deprecated — progress moved to in-world gold-orb VFX in drawEvent).
  • Tutorial overlay system: TUTORIAL_STEPS map, TUTORIAL_DURATION = 3 s, proximity-step set {5, 6, 7, 8}, localStorage key 'ss_tutorials_seen', persistence helpers _loadGlobalSeen / _persistGlobalSeen, and exports triggerTutorial, updateTutorial, setProximityTutorial, markProximityTutorialDone, resetTutorial, resetAllTutorials, plus the in-file _drawTutorialOverlay and _drawTutorialDismiss glitch animation.
  • Reward-cards pipeline (level-up cards, upgrade-show, banish targeting, merge cinematic): updateRewardState, startRewardReveal, startUpgradeShowOnly, startMergeCinematic, fadeoutReward, prepareUpgradeShow, cancelReward, isRewardActive, isRewardShowing, startCardVanish, isAnyVanishing, resetCardVanishState, resetAutopickCountdown, isBanishTargeting, isCardSlotDead, getSelectedCardIndex, setSelectedCard, getAutoPickCountdown, shouldAutoPick, toggleAutopickPause, isAutopickPaused, hitTestPauseBtn, hitTestRerollBtn, hitTestBanishBtn, setSlotIntroDuration, drawRewardCard, drawRewardCards, drawAnimatedBanner, drawBanishTargetingBanner.
  • Damage flash + death cinematic: drawDamageFlash(ctx) and drawDeathCinematic(ctx, deathElapsed) are called outside the main HUD pass by bridge.ts.
  • Cached HUD slot screen positions: _weaponSlotPos, _modSlotPos, _artSlotPos (mutated by drawHUD each frame, read by the reward upgrade-show animation to target HUD badges). getTopArtifactSlotPos() exposes the topmost artifact slot to the activation-banner feed.
  • HUD bounds: _hudL, _hudR, _hudCx. Default to full canvas; setHudBounds(left, right) lets the playground narrow the band to a centered portrait guide.
  • Hit-test exports: hitTestCog, hitTestHelp, hitTestMusicPlayer, hitTestWeaponSlot. The weapon-slot hit test recomputes the same dynamic sizing math as the draw pass and accepts a forgiving radius of wR + 8 * uiScale. hitTestMusicPlayer reads the previously cached _musicPlayerBounds, though that bounds object is now always null because the music player moved to a React overlay (the export is kept for back-compat).
  • One-shot animation triggers: flashBossBar() (sets _bossBarFlash = 1), triggerCogPressAnim() (sets _cogPressT = 1), pushHitIndicator(angle), pushWarning(...), dismissWarning(text), setHudControlMode(mode).

READS FROM

  • ../core: game, ship, world, W, H, uiScale, playerInput, camera — every layout calc and every state read.
  • ../core/clock: Clock (no .now() direct in HUD draw, but imported for the reward pipeline timing).
  • ../core/config: PERF_FLAGS.
  • ../core/fps: getSmoothedFps().
  • ../core/types: WorldState type.
  • ../audio/micro-sfx: MicroSfx for reward-pipeline sound cues.
  • ../audio/music-player: MusicPlayer.
  • ../player/states: hasExclusiveState(ship, 'starpower') for the heat-bar Star Power swap.
  • ../world/leveling: XP_THRESHOLDS (extended live each frame by LevelingSystem.update).
  • ../world/artifacts: getActiveArtifacts(), getArtifactFlash(id).
  • ../world/props: getPropPool() for supply-pod edge arrows.
  • ../../data/kill-streaks: STREAK_MILESTONES.
  • ../../data/modifiers: MODIFIER_TYPE_MAP.
  • ../../data/artifacts: ARTIFACT_MAP, ARTIFACT_TIER_COLOR_BY_IDX.
  • ../../data/bosses: BOSS_DEFS.
  • ../../data/weapons: WEAPON_MAP, resolveWeaponRarity, type DamageTag.
  • ../../data/weapon-icons: WEAPON_EMOJIS.
  • ./renderer: PAL.
  • ./fire-engine: FireEngine — fed the green XP-bar flame stamp each frame.
  • ./weapon-icons: getWeaponIconImg, getOrBakeEmojiIcon.
  • ./ui-icons: getUiIcon('heart'), getUiIcon('gear').
  • ./damage-tag-pill: drawDamageTagPill.
  • ./mission-timer-hud: drawMissionTimerBanner.
  • ./reward-cinematics: getCinematicFor, resetActiveCinematic, type CinematicContext.
  • ./card-theme: rarity gradients, badge tables, paintCardGradient, type Rarity.
  • ./medical-canvas-palette: MedicalPalette, medFontHead, medFontBody, medApplyPanelShadow, medClearShadow.
  • Player-state reads: ship.hp, ship.hpMax, ship.shield, ship.shieldMax, ship._shieldBackgroundRegen, ship._shieldRecovering, ship.heat, ship.vx/vy, ship.weapons, ship.stalled, ship.alive, ship.exclusiveStateTimer, ship._exclusiveStateMaxTimer.
  • Game-state reads: game.phase, game.time, game._dt, game._rawDt, game.xp, game.level, game._currentLevel, game.missionTimer, game.overtime, game.overtimeElapsed, game.overtimeSkulls, game.killStreak, game.lastStreakMilestone, game.upgradeCounts, game.weaponSlotsMax, game.portal, game.tutorialStep, game._magnetIconTimer, game._activeBossDefId, game.currentReward.

PUSHES TO

  • The supplied CanvasRenderingContext2D — only side-effect output of the render functions themselves.
  • MicroSfx — reward-card juice and banner sfx (level-up, reward reveal, vanish, banish).
  • FireEngine.setColor / addScreen / flush / getCanvas — XP-bar block stamps a 0.2-alpha green flame band beneath the fill via 'lighter'.
  • PostFx — reward and merge cinematics spawn post-effects.
  • localStorage['ss_tutorials_seen'] — comma-separated list of globally-seen tutorial step ids.
  • Module-local state owned here: _xpFlash, _prevXp, _prevHpMax, _prevShieldMax, _hpGainFlash, _shieldGainFlash, _hpGainFrac, _shieldGainFrac, _cogPressT, _cogHitBox, _helpHitBox, _musicPlayerBounds, _hitIndicators, _lanternFill, _lanternVisible, _lanternAlpha, _bossBarT, _bossBarSmoothHp, _bossBarLastHpMax, _bossBarLastName, _bossBarLastColor, _bossBarFlash, warnings, warningCooldowns, _weaponSlotPos, _modSlotPos, _artSlotPos, _slotAnims, _hudL, _hudR, _hudCx, _controlMode, and the entire reward-cards state machine in the lower half of the file.

DOES NOT

  • Does not run physics, collision, AI, weapon firing, cooldown ticking, XP awarding, or any game logic — it only reads the current frame’s state.
  • Does not produce in-world rendering. The world (entities, terrain, bullets, particles, lighting) is drawn by renderer.ts and draw.ts before drawHUD runs.
  • Does not own the render-pass order. bridge.ts decides when drawHUD, drawDamageFlash, drawRewardCards, and drawDeathCinematic run relative to the world and post-FX.
  • Does not own the boss-bar damage coalescer — bridge.ts calls flashBossBar() after the per-frame damage flush.
  • Does not handle DOM input. Touch / mouse events are dispatched by GameScreen.tsx and ShipPlaygroundScreen.tsx, which then call into the hit-test exports or the reward-card pipeline.
  • Does not call ctx.clip() inside the main HUD draw path except for the bar-fill gloss and dashed-marker overlays in _drawPremiumBar and the shield-regen ghost; the file’s perf contract still bans clip regions in the high-frequency layout layer.
  • Does not create per-frame gradients in the small-budget portions (FPS, killstreak, modifier slots, artifact slots, mission timer, hit chevrons). Gradients are confined to the premium HP/shield bars, the XP-bar fill, and the heat-bar fill.
  • Does not draw the React-side music player. _musicPlayerBounds is always null today; the player is the React overlay MusicPlayerWidget in GameScreen.tsx.
  • Does not own artifact activation banner rendering — that lives in ./draw-artifact-banners, which uses getTopArtifactSlotPos() to anchor itself to the top artifact slot.
  • Does not write to Supabase, Sentry, or any persistent store outside the localStorage tutorial flag.

Signals

  • flashBossBar() — one-shot, called by bridge.ts after damage events accumulate; sets _bossBarFlash = 1, decayed via easeOutCubic over 0.25 s in wall-dt.
  • triggerCogPressAnim() — one-shot, called on cog tap-down; sets _cogPressT = 1, damped sine wobble over COG_PRESS_DUR = 0.28 s.
  • pushHitIndicator(angle) — called from damagePlayer() (combat/damage.ts) when the player takes damage from a known angle.
  • pushWarning(text, color, glow, duration, priority, bg?, stroke?) and dismissWarning(text) — called by gameplay systems for overheat, shield-broken, stalled, level-up callouts, no-weapon-slot, exit-portal, and similar prompts.
  • setHudBounds(left, right) — ship-playground signal that narrows the HUD band to a centered portrait guide; (0, 0) resets to full canvas.
  • setHudControlMode('touch'|'mouse'|'arrows') — called by GameScreen whenever the active control mode changes.
  • setLanternFill(fill, visible)@deprecated no-op kept for back-compat. Lantern progress now lives in drawEvent as a gold-orb VFX.
  • _xpFlash is set to 1 whenever game.xp > _prevXp is detected during the XP-bar block — implicit signal driven by gameplay simply increasing game.xp.
  • HP / shield gain flashes are triggered implicitly when ship.hpMax or ship.shieldMax grows between frames.

Entry points

  • bridge.ts — primary importer. Calls drawHUD(ctx) inside the main per-frame render pass at line 7427 (after the world and post-FX passes) and again at line 4027 in the rewards-only short-path. Also imports drawDamageFlash, drawDeathCinematic, drawRewardCards, pushWarning, dismissWarning, the whole reward-cards lifecycle, the tutorial lifecycle, getBossBar, flashBossBar, and the deprecated setLanternFill.
  • renderer.tsRenderer.renderPasses(...) takes drawHUD as a function arg (the only HUD pass) and invokes it after post-FX and lighting overlays.
  • GameScreen.tsx — calls setHudControlMode, setHudBounds, triggerCogPressAnim, hitTestCog, hitTestHelp, hitTestWeaponSlot, hitTestMusicPlayer, and the reward-card hit tests.
  • ShipPlaygroundScreen.tsx — calls setHudBounds to narrow the HUD to a portrait band when split panels are open; also imports the reward-card lifecycle for testing.
  • combat/damage.ts — calls pushHitIndicator(angle) when the player takes damage.
  • mission-timer-hud.ts — paired module, called inline by drawHUD via drawMissionTimerBanner(ctx) near the start of the pass.
  • draw-artifact-banners.ts — reads getTopArtifactSlotPos() to anchor the artifact-activation banner column.

Pattern notes

  • Single perf-budgeted entry. The file header lists the < 2 ms budget and the explicit bans (no clip in the bar layout layer, no per-frame gradient creation in small slots, no shadowBlur, no globalCompositeOperation flips outside the listed weapon-glow paths, font set ≤ 3-4 times per frame). All non-circular geometry uses axis-aligned fillRect; the only ctx.arc count of consequence is the weapon-slot GCD ring and per-slot ready-glow rings.
  • Layout uses an HUD band, not the full canvas. _updateHudBounds() runs at the top of every drawHUD and resolves _hudL / _hudR (defaulting to 0..W) and _hudCx. Every HUD element anchors to the band, so the ship playground’s portrait-width split panel works without touching the draw code.
  • topOff accommodates the boss bar slide. topOff = round(24 * uiScale) + getBossBarHudOffset() pushes the HP/shield bars, mission timer, modifier and artifact columns down by the boss bar’s current expanded height so the bar can slide in without overlap. _drawBossBar returns the same offset for the XP bar to consume.
  • Wall-dt for cosmetic tweens. Boss bar slide / fade / damage flash all use game._rawDt (BOSS_BAR_SLIDE_IN, BOSS_BAR_FADE_OUT, BOSS_BAR_FLASH_DUR). This deliberately keeps the bar animating through pause and slow-mo so the player still gets a clean enter/exit even when game-dt is frozen.
  • Slot-position cache for upgrade-show. Every frame, the weapon / modifier / artifact loops rewrite _weaponSlotPos, _modSlotPos, _artSlotPos. The reward upgrade-show pipeline reads those arrays as the destination for the badge “fly home” animation, so HUD layout changes propagate to the cinematic for free.
  • Premium bars overlap the XP bar by 1 uiScale px. barY = m + topOff - 1 * uiScale parks the HP/shield bars flush against the bottom of the XP bar. Slanted parallelogram geometry (SLANT = 0.08) is traced manually via arcTo — explicit because no canvas primitive provides slanted rounded rects.
  • Weapon icon desaturation as the cooldown read. Instead of a numeric cooldown label, ctx.filter = 'saturate(0)' is applied to the weapon icon while cdPct > 0.02. Combined with the yellow GCD arc, the slot reads as available/unavailable at a glance.
  • Per-weapon fire-flash juice patterns. A 13-branch switch in the weapon-slot loop drives the per-weapon squash/stretch/rotate/jitter under _fireFlash > 0 (decayed elsewhere over ~0.2 s). New weapons get a generic fallback (+14 % horizontal, -8 % vertical, slight rotate).
  • Star Power overrides the heat-fill gradient. When hasExclusiveState(ship, 'starpower') is true, the heat bar shows the remaining Star Power timer drained instead of ship.heat / 100, with a 7-stop pastel-rainbow hsl gradient driven by game.time * 240 and the danger flash + overheat emoji suppressed.
  • Warnings sit on the modern medical hierarchy. Most warnings render via _drawText3DShadow (white fill, stacked black 3D shadow, Space Grotesk 700). The combat-urgency set ('OVERHEATING', 'STALLED', 'SHIELD BROKEN') keeps the legacy bold Cal Sans + colored fill so the urgent cues still punch.
  • Boss bar identity vs. pool. The bar’s HP pool sums every alive enemy with sharesHealthWithBoss; its name and color come from BOSS_DEFS[game._activeBossDefId] so multi-body bosses (e.g. Prism Cluster) show the encounter title and color rather than a per-host name. Falls back to the live isBoss anchor’s displayName / barColor when the def lookup misses.
  • HUD is the dumping ground for everything overlay-shaped. Beyond bars and slots, the file owns the entire reward-cards UI, upgrade-show, merge cinematic, banish targeting, damage flash, death cinematic, and tutorials. The single hud.ts file is over 6000 lines as a result, but the perf contract for drawHUD itself is enforced narrowly on the per-frame layout layer.