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 whengame.phase === 'menu'or!ship.aliveor when the HUD band is under 100 CSS px wide.- HP and shield premium-bar layout: helpers
_drawPremiumBar,_drawIconBadge,_slantedRect. ConstantsbarH = 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 whenship.hpMaxorship.shieldMaxincreases 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-jadexpGradplus vertical jade-edge overlay, centered “Level X” text in Cal Sans medical head 700 with stacked dark stroke, and a white whiteout_xpFlashtriggered ongame.xpincrease, decayed at 4×/s. ReadsXP_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 to10 * uiScale),wpnBaseY = H - m - wR - 18 * uiScale. Per-slot: dark#080f1abackground disc, optional ready golden glow (#fbbf24, three'lighter'ring strokes, only whenw.fireTimer <= 0.02), rarity-colored border ring fromRARITY_HEX, weapon emoji fallbackWEAPON_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 viagetWeaponIconImg(w.id), per-weapon juice on_fireFlash > 0(recoil/squeeze/shake/jitter patterns keyed byw.idforrifle,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 (#3bb4ffenergy /#ff3b3bexplosion /#ff8a1cfire /#ffffffbullet), burnout#222222gray + red X whenship.stalled, weapon name label under the slot ('EMPTY'for empty slots), and damage-tag pills under the name viadrawDamageTagPill. Empty slots render a dim#444455outline. - Modifier slots column (top-right):
modR = 14 * uiScale, vertical stack of only FILLED slots (no empty circles, infinite count) fromgame.upgradeCounts. Dark navy disc, icon viagetWeaponIconImg('mod_' + id)orgetOrBakeEmojiIcon(modDef.icon), level badge bottom-right. - Artifact slots column (top-right, one column left of modifiers):
artR = 18 * uiScale. Per-artifact pulse stroke (tierColorfromARTIFACT_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 bygetArtifactFlash(inst.id)— scale overshoots to ~1.5× with white overlay disc, bright outer ring (def.colors.bright || .primary, 4 ×uiScalewidth), 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 (_bossBarTdriven bygame._rawDtso it plays through pause and slow-mo), smoothed HP (_bossBarSmoothHpeases at ~12/s toward the live sum, drains at ~6/s during fade-out),_bossBarFlashdamage pulse (eased overBOSS_BAR_FLASH_DUR = 0.25s), constantsBOSS_BAR_SLIDE_IN = 0.4,BOSS_BAR_FADE_OUT = 0.6,BOSS_BAR_HEIGHT = 28pre-uiScale. Renders semi-transparent black background, colored fill (def.barColor), white flash overlay, hairline border, uppercase name with 3-line stacked shadow, and right-alignedhp / hpMaxnumerals.getBossBar(worldState)is exported and computes the bar pool from every alive enemy withsharesHealthWithBoss; bar identity comes fromBOSS_DEFS[game._activeBossDefId]. - Mission timer block (top-right, big Cal Sans head 600): countdown when
!game.overtime; under 30 s pulsesMedicalPalette.statusBad; in overtime, switches to count-upOT M:SSflashing betweenMedicalPalette.magentaandMedicalPalette.statusBad(6 Hz), with up to 20 skull glyphs (☠) stacked left of the timer one per 15 s of overtime. Below the timer, level indicatorLv.NinMedicalPalette.accentBrightcyan whengame._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 fromSTREAK_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 byship.heat / 100. Star Power swaps in a pastel rainbow hue-cycle driven bygame.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), showsnKover 1000, color#ff9944over 9000). Fire emoji below the bar whileplayerInput.isThrusting && !starPowerActive, pulsing red 2 → 12 Hz oncehP >= 0.80. - Cog (bottom-left) + help
?(bottom-right) buttons. Cog uses thegearUI sprite with damped-sine squash/stretch overCOG_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[]withtext/color/glow/life/duration/priority/bg?/stroke?, per-textwarningCooldowns(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 ongame.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#ff2222rectangles atmin(W, H) * 0.42from screen center, with a#ff8888inner core. - Portal indicator
_drawPortalIndicator: amber#dd88ffchevron at the screen edge always pointing towardgame.portalwhile the portal exists and isn’t'entered', plus an additional bobbing downward chevron above the portal when on-screen. Pulse0.6 + 0.4 * sin(game.time * 4). - Supply-pod arrows
_drawSupplyPodArrows: iteratesgetPropPool().forEachEdgeArrowProp(...)and renders amber#ffcc66edge chevrons / on-screen bobbing pointers, with a “dramatic” hot-pulse modulation for pods more thanDRAMA_DIST_PX = 900away. - Magnet flash icon: black-body / red-tip magnet symbol with white radial-gradient glow, driven by
game._magnetIconTimerover a 1.5 s lifetime with 18 Hz flash. - Lantern fill meter (
_lanternFill,_lanternVisible,_lanternAlpha): retained as a back-compat hook;setLanternFillis now a no-op (@deprecated— progress moved to in-world gold-orb VFX indrawEvent). - Tutorial overlay system:
TUTORIAL_STEPSmap,TUTORIAL_DURATION = 3 s, proximity-step set{5, 6, 7, 8}, localStorage key'ss_tutorials_seen', persistence helpers_loadGlobalSeen/_persistGlobalSeen, and exportstriggerTutorial,updateTutorial,setProximityTutorial,markProximityTutorialDone,resetTutorial,resetAllTutorials, plus the in-file_drawTutorialOverlayand_drawTutorialDismissglitch 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)anddrawDeathCinematic(ctx, deathElapsed)are called outside the main HUD pass bybridge.ts. - Cached HUD slot screen positions:
_weaponSlotPos,_modSlotPos,_artSlotPos(mutated bydrawHUDeach 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 ofwR + 8 * uiScale.hitTestMusicPlayerreads 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:WorldStatetype.../audio/micro-sfx:MicroSfxfor 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 byLevelingSystem.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, typeDamageTag.../../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, typeCinematicContext../card-theme: rarity gradients, badge tables,paintCardGradient, typeRarity../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.tsanddraw.tsbeforedrawHUDruns. - Does not own the render-pass order.
bridge.tsdecides whendrawHUD,drawDamageFlash,drawRewardCards, anddrawDeathCinematicrun relative to the world and post-FX. - Does not own the boss-bar damage coalescer —
bridge.tscallsflashBossBar()after the per-frame damage flush. - Does not handle DOM input. Touch / mouse events are dispatched by
GameScreen.tsxandShipPlaygroundScreen.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_drawPremiumBarand 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.
_musicPlayerBoundsis always null today; the player is the React overlayMusicPlayerWidgetinGameScreen.tsx. - Does not own artifact activation banner rendering — that lives in
./draw-artifact-banners, which usesgetTopArtifactSlotPos()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 bybridge.tsafter damage events accumulate; sets_bossBarFlash = 1, decayed viaeaseOutCubicover 0.25 s in wall-dt.triggerCogPressAnim()— one-shot, called on cog tap-down; sets_cogPressT = 1, damped sine wobble overCOG_PRESS_DUR = 0.28 s.pushHitIndicator(angle)— called fromdamagePlayer()(combat/damage.ts) when the player takes damage from a known angle.pushWarning(text, color, glow, duration, priority, bg?, stroke?)anddismissWarning(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 byGameScreenwhenever the active control mode changes.setLanternFill(fill, visible)—@deprecatedno-op kept for back-compat. Lantern progress now lives indrawEventas a gold-orb VFX._xpFlashis set to 1 whenevergame.xp > _prevXpis detected during the XP-bar block — implicit signal driven by gameplay simply increasinggame.xp.- HP / shield gain flashes are triggered implicitly when
ship.hpMaxorship.shieldMaxgrows between frames.
Entry points
bridge.ts— primary importer. CallsdrawHUD(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 importsdrawDamageFlash,drawDeathCinematic,drawRewardCards,pushWarning,dismissWarning, the whole reward-cards lifecycle, the tutorial lifecycle,getBossBar,flashBossBar, and the deprecatedsetLanternFill.renderer.ts—Renderer.renderPasses(...)takesdrawHUDas a function arg (the only HUD pass) and invokes it after post-FX and lighting overlays.GameScreen.tsx— callssetHudControlMode,setHudBounds,triggerCogPressAnim,hitTestCog,hitTestHelp,hitTestWeaponSlot,hitTestMusicPlayer, and the reward-card hit tests.ShipPlaygroundScreen.tsx— callssetHudBoundsto narrow the HUD to a portrait band when split panels are open; also imports the reward-card lifecycle for testing.combat/damage.ts— callspushHitIndicator(angle)when the player takes damage.mission-timer-hud.ts— paired module, called inline bydrawHUDviadrawMissionTimerBanner(ctx)near the start of the pass.draw-artifact-banners.ts— readsgetTopArtifactSlotPos()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
clipin the bar layout layer, no per-frame gradient creation in small slots, noshadowBlur, noglobalCompositeOperationflips outside the listed weapon-glow paths, font set ≤ 3-4 times per frame). All non-circular geometry uses axis-alignedfillRect; the onlyctx.arccount 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 everydrawHUDand resolves_hudL/_hudR(defaulting to0..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. topOffaccommodates 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._drawBossBarreturns 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
uiScalepx.barY = m + topOff - 1 * uiScaleparks the HP/shield bars flush against the bottom of the XP bar. Slanted parallelogram geometry (SLANT = 0.08) is traced manually viaarcTo— 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 whilecdPct > 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 ofship.heat / 100, with a 7-stop pastel-rainbowhslgradient driven bygame.time * 240and 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 fromBOSS_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 liveisBossanchor’sdisplayName/barColorwhen 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.tsfile is over 6000 lines as a result, but the perf contract fordrawHUDitself is enforced narrowly on the per-frame layout layer.