PURPOSE

Canvas-2D entity drawing primitives — ship, enemies, bullets, terrain, pickups, shields, HP bars, event signposts, damage numbers, and regen stations. Ported from the legacy 08b-render-gameplay.js. Every per-frame world-space drawable except the post-effects pipeline lives in this module.

OWNS

  • Player ship draw: drawShip, drawShipSilhouette, hi-res sprite cache _hiResMap, setActiveShipRarity, setActiveHullClass, getShipDrawSize, getShipVisualScale.
  • Ship-scale constants: SHIP_DRAW_SCALE = 6.3, SHIP_SPRITE_PAD_MULT = 1.6, RARITY_SIZE_MULT (common 0.92 → legendary 1.12).
  • Enemy + shape rendering: drawEnemy, drawShape, drawShapeSilhouette, drawEnemySilhouette.
  • Bullet rendering: drawBullet — single switch over arch with branches for pulse_ball, cannonball, projectile, explosive, sniper, beam, chain, homing, mortar_shell, blade/sweep_laser, ember, disc_ring, shield_arc, tesla_line, mega_bullet, plasma_arc, quad_round, phoenix_pulse, plasma_ball, plasma_fire_zone, fire_patch, carpet_bomber, star_halo_root, beam_decay, artillery_strike, plus a generic fallback circle.
  • Prebaked stamps: missile rocket stamp (_getMissileStamp, 64px), XP-orb stamps in four power-of-two tiers (32/64/128/256, picked by _pickXpOrbTier), shield ring stamp (_getShieldRingStamp, quantized to 2px), shield top-shell stamps (128px).
  • Shield rendering: drawShield, drawShieldBottom, drawShieldTop, drawShieldRingShadow, getShieldVisualRadius, RARITY_SHIELD_COLORS, base palette SHIELD_COLOR/SHIELD_COLOR_LITE/SHIELD_COLOR_DEEP, plus internal rim/hit-flash/ripple/rune helpers.
  • HP UI in world space: triggerHpFlash, drawHpBar, drawHpRingShadow (uses SHADOW_OFFSET = 3, SHADOW_BLUR = 6, HP_FLASH_DURATION = 0.5).
  • Pickups + orbs: drawPickup, drawXpOrb (animated via XP_ORB_BOB_AMP, XP_ORB_BOB_SPEED, XP_ORB_SWIRL_SPEED, XP_ORB_SWIRL_COUNT, XP_ORB_TWINKLE_PERIOD, XP_ORB_TWINKLE_WINDOW, XP_ORB_MAX_SCALE).
  • Terrain: drawTerrainFill, drawTerrainMergedStroke, drawTerrain, internal buildTerrainPath, offscreen-buffer helper ensureOffscreen.
  • Misc effects: drawExplosion, drawGlow, drawJunk, drawAllDamageNumbers (with cached _dmgTextCache, _fmtDmg, _getDmgCanvas).
  • Events: drawEvent, drawEventSignpost, drawEventStars, drawRegenStations, EVENT_COLORS table, COIN_NEON color table, helpers _starPath, _drawFivePointStar, _drawEventFlash.

READS FROM

  • ../core: camera, uiScale, game, ship, playerInput, world — view transform, current time, player position.
  • ../core/clock: Clock.now() for time-based pulse/breath animations independent of game.time.
  • ../core/config: PERF_FLAGS for perf-gated rendering paths.
  • ./camera: Camera.toS(x, y) — world-to-screen projection used by every branch.
  • ./shapes: Shapes, ShapeDef — polygon data for ship/enemy silhouettes.
  • ./renderer: PAL (XP and palette colors).
  • ./ships-v4-loader: getShipV4 — v4 hull-class diffuse texture lookup.
  • ./glow-stamp: drawGlow as _drawGlow, drawWhiteGlow as _drawWhiteGlow — additive color and white-hot stamps used by the universal projectile glow pass.
  • ./weapon-colors: getWeaponColor(_weaponId) — RGB float color keyed by weapon id for the neon glow pass.
  • ../vfx/juice: ShipRecoil — recoil offset applied to the ship draw.
  • ../vfx/post-fx: PostFx.spawn(...) — sniper beam afterimage spawns a 'scar' PostFx on the first frame.
  • ../vfx/squash-stretch: getShipSquash, getEnemySquash — per-entity squash multipliers.
  • ../../data/ships: type ShipRarity.
  • Bullet branches read per-bullet hints from the live bullet object: b.weaponId, b.weaponLevel, b._beamAngle, b._beamRange, b._beamWidth, b._beamLingerAge, b._beamHitboxSec, b._hideBeamLine, b._phaseAlpha, b._orbitAngle, b._orbitRadius, b._beamLengthFrac, b._bladeSize, b.c1/c2/c3, b._arcHeight, b._artPhase, b._artBlastR, b._artLandX/Y, b._artTelegraphProgress, b._ghostSpawned. Anything missing falls back to a hardcoded constant.

PUSHES TO

  • The supplied CanvasRenderingContext2D (only side-effect output of the draw calls themselves).
  • PostFx.spawn('scar', ...) from the sniper branch — one persistent ghost scar per railgun beam, marked via b._ghostSpawned so subsequent frames don’t respawn.
  • Internal stamp caches (_hiResMap, _shieldRingMap, _xpOrbStamps, _missileStamp, _dmgTextCache, _shieldTopShellCache) — lazily populated on first request.
  • Module-local mutable state: _activeShipRarity, _activeHullClass, HP-flash timer, ghost-scar spawn flag on the bullet.

DOES NOT

  • Does not own the render loop or pass order — bridge.ts calls these functions in the correct order each frame.
  • Does not run physics, collision, AI, input, or game logic. radius for collision is passed in unchanged; the visual sz derived inside drawBullet does not feed back into hit-testing.
  • Does not own beam hitscan, sword orbit, gravity-mine logic, sweep-laser orbit motion, or mortar arc physics — only their visual representation.
  • Does not draw cone-beam shader effects, warp puddles, smoke, thrusters, screen-space post-FX, or HUD overlays — those live in sibling modules under engine/rendering/ and engine/vfx/.
  • Does not enforce frame budgeting or feature-flag gating beyond reading PERF_FLAGS.
  • Does not write to Supabase, telemetry, or any persistent store.

Signals

  • drawBullet writes telemetry only via the implicit canvas draws; it does not log or measure on its own.
  • setActiveShipRarity / setActiveHullClass are call-site signals from the run-assembly layer — once set, every drawShip call uses them until the next override.
  • triggerHpFlash(intensity) is a one-shot signal that starts a flash timer consumed by the next drawHpBar calls over HP_FLASH_DURATION seconds.
  • Sniper branch sets b._ghostSpawned = true to signal “scar already issued for this bullet” to subsequent frames.
  • Sweep-laser branch reads b._beamLengthFrac as an emergence-animation signal owned by the bullet update.

Entry points

  • bridge.ts calls drawBullet(ctx, b.x, b.y, b.rad, b.c1, 1, bArch, b.c2 || '#ffffff', b.vx, b.vy, wid, b) once per active bullet during the per-frame render pass.
  • bridge.ts is the single importer of most draw functions and stitches them into the frame in order: terrain fill → terrain stroke → pickups/XP orbs → enemies → bullets → ship → shield bottom/top → HP ring/bar → event signposts/stars → regen stations → damage numbers → junk.
  • combat/damage.ts is the only other importer — it calls into damage-number drawing helpers.
  • Many small exports (drawShipSilhouette, drawEnemySilhouette, getShipVisualScale, getShieldVisualRadius, getActiveShipRarity) are public helpers for UI/HUD modules.

Pattern notes

  • Branch-on-archetype in drawBullet. All projectile variety is a single linear if (arch === 'X') / else if ... switch on arch, with a generic fallback that paints a soft additive halo + solid colored body + white center dot. There is no archetype registry; new bullet types are added by editing one branch in this file.
  • Legendary VFX scale. A bullet is treated as legendary when b.weaponId starts with lgd_. Visual size multiplier _lgdVfxScale = 0.15 + (lvl - 1) * 0.35 / 19, clamped via the (1..20) clamp on _lgdLvl, gives 0.15× at L1 climbing linearly to 0.50× at L20. Visual alpha is also nerfed to 0.55 (_lgdAlphaScale) for legendaries on every branch. Damage/collision radii are untouched — only the rendered sz shrinks.
  • BULLET_VISUAL_SCALE = 1.04. Replaces an older 0.8 factor and intentionally keeps the VFX slightly smaller than the collision radius so projectiles read crisp but hitboxes stay generous.
  • Universal-glow inline pass is disabled. The early-bullet universal-glow block at the start of drawBullet is gated behind if (false && ...), with a no-op skip list NO_UNIVERSAL_GLOW = ['sniper', 'orbit', 'gravity_mine', 'shield_arc', 'tesla_line', 'homing', 'mortar_shell']. Kept for historical reference but does not run.
  • Tail neon glow pass. After the archetype switch, every projectile body is followed by a unified additive glow drawn via _drawGlow (outer color stamp) + _drawWhiteGlow (inner white-hot core). Radius is Math.max(15, sz * 6), doubled to × 1.8 for cannonball. This pass is skipped for the archetypes that do their own bespoke glow: sniper, star_halo_root, beam_decay, artillery_strike, phoenix_pulse, plasma_fire_zone, fire_patch, carpet_bomber, mega_bullet. Color is keyed off _weaponId via getWeaponColor.
  • Draw-call layering within a branch. Every bullet branch follows the same stacking order: optional ground shadow → wide soft additive halo (globalCompositeOperation = 'lighter') → mid color glow → solid colored body ('source-over') → outline if any → white-hot center dot/streak → optional secondary effects (crackle, glitch lines, endcap orb). Stacks of three or more concentric arcs with decreasing radius and increasing alpha are the dominant idiom.
  • ctx.save()/restore() discipline. drawBullet opens one outer save, then a second save before the tail neon pass to undo per-branch translate/rotate transforms. Branches that need their own coordinate frame save/restore internally (e.g. homing for missile sprite rotation).
  • Hi-res sprite cache. _getHiResShip pre-bakes each loaded ship PNG to a 2× offscreen canvas keyed by sprite.src and returns null if the image isn’t decoded yet, so the polygon fallback path can run.
  • Stamp baking pattern. Shields, missiles, XP orbs, and damage numbers all bake static glyphs once into an offscreen canvas and animate via globalAlpha per frame — no per-frame shadowBlur (which is the perf hot path the comments call out). Shield ring stamps quantize their radius to the nearest 2px to keep the cache small.
  • Sniper beam two-phase fade. lingerAge < hitboxSec (~0.10s) holds full brightness; afterwards an ease-out (1 - decayT)^1.6 runs over the remaining lifetime up to maxLifetime (~1.0s). The first full-brightness frame spawns a PostFx 'scar' that fades over 4 seconds, providing the ghostly afterimage.
  • Color sourcing. Bullet colors come from the bullet itself (color, color2, b.c1/c2/c3) for branch-internal strokes, but the tail neon glow uses getWeaponColor(_weaponId) so the halo follows the weapon’s canonical palette regardless of per-bullet overrides.