PURPOSE

Loads, caches, and downsamples the eight raw enemy archetype PNGs that live in public/enemies/. Provides the lookup that the bridge’s atlas-patch step reads to bake red-tinted, outlined enemy silhouettes into the sprite atlas (one common variant plus one variant per rarity color per archetype). Pipeline is fully separate from the player /ships-v4/ sprite pipeline.

OWNS

  • ENEMY_SPRITE_MAP — archetype-string to PNG-path table. Two factions, four archetypes each: Bugs (shooter, orb, mortar, charger) and City (gunner, field, sniper, racer).
  • Module-private _cache: Record<string, HTMLImageElement> — keyed by archetype, holds the downsampled image returned by downsampleSprite.
  • Module-private _loaded: boolean — single-shot guard so preloadEnemySprites is idempotent.
  • Exported functions: preloadEnemySprites, getEnemySprite, archetypeFromTypeId.

READS FROM

  • SPRITE_TARGET_ENEMY from ../core/config — the target dimension passed to downsampleSprite for every enemy sprite.
  • downsampleSprite from ./sprites — used to shrink each loaded HTMLImageElement to SPRITE_TARGET_ENEMY before caching.
  • The browser’s Image loader, fetching files at /enemies/<archetype>.png (eight static assets under public/enemies/).

PUSHES TO

  • Its own in-module _cache — never exposed directly; readers go through getEnemySprite.
  • console.warn on load failure (network error, zero-dimension image) — diagnostic only, never throws.
  • Caller (the atlas patcher in engine/bridge.ts) — getEnemySprite(archetype) returns the cached HTMLImageElement which the caller draws into a 128-px scratch canvas, source-in fills red, then outlines per rarity before patching the atlas region.

DOES NOT

  • Does not draw, blit, or composite. No ctx.drawImage or atlas writes happen here — those are bridge-side.
  • Does not tint, outline, or recolor the sprites. The red silhouette and the per-rarity outline/glow are baked downstream by the bridge atlas-patch step.
  • Does not handle bosses or boss anchors. Boss sprites come from public/ships-v4/ via BOSS_TYPE_TO_HULL, not from this module.
  • Does not share assets with the player /ships-v4/ pipeline. If a player ship needs reuse as an enemy, the PNG is duplicated under public/enemies/.
  • Does not block on missing sprites. A failed load resolves silently; the renderer falls back to polygon rendering until the sprite is ready (same contract as player ship sprites).
  • Does not retry failed loads. One attempt at boot; failure is terminal for that session.
  • Does not encode rarity. Rarity is appended downstream when the bridge builds atlas region keys (enemy_<archetype>_<rarity>).

Signals

  • console.warn('[enemy-sprites] Failed to load: <path>') on img.onerror.
  • console.warn('[enemy-sprites] Zero dimensions: <path>') when the image loads but reports naturalWidth or naturalHeight of 0.
  • No success log, no telemetry event, no return value other than the resolved Promise<void> from preloadEnemySprites.

Entry points

  • preloadEnemySprites(): Promise<void> — called once during boot from engine/core/preload.ts as step 2 of the preload sequence (after preloadAllShipV4, before preloadWeaponIcons). Concurrently loads all eight PNGs via Promise.all over Object.entries(ENEMY_SPRITE_MAP); each entry downsamples on onload and resolves regardless of success or failure.
  • getEnemySprite(archetype: string): HTMLImageElement | null — called from engine/bridge.ts inside the per-archetype atlas-patch loop (_ALL_ENEMY_ARCHETYPES = ['orb', 'charger', 'shooter', 'mortar', 'gunner', 'field', 'sniper', 'racer']). Returns null if the cache entry is missing, the image is not complete, or naturalWidth <= 0; callers continue past archetypes that aren’t ready and retry on the next render pass.
  • archetypeFromTypeId(typeId: string): string — splits a typeId on _ and returns the first segment. Maps full typeIds like shooter_common or orb_legendary back to the archetype key used by ENEMY_SPRITE_MAP and getEnemySprite.

Pattern notes

  • Archetype to PNG mapping is one-to-one and static. The ENEMY_SPRITE_MAP keys are the authoritative archetype names; _ALL_ENEMY_ARCHETYPES in engine/bridge.ts must stay in sync, and atlas slots in atlas-builder.ts must reserve enemy_<archetype>_<rarity> for every (archetype, rarity) pair.
  • Rarity is encoded in the typeId suffix, not in the sprite. A single archetype PNG feeds every rarity tier — the bridge tints once to red, then bakes one common outline plus one outline-plus-glow per rarity color (ENEMY_RARITY_COLORS) into separate atlas regions all sourced from the same HTMLImageElement.
  • archetypeFromTypeId is the canonical decoder for typeIds at the rendering boundary. It assumes typeIds always follow <archetype>_<rarity> and does no validation — a typeId without an underscore returns the whole string unchanged.
  • Idempotent preload via the _loaded guard; safe to re-call but a no-op after first completion.
  • Non-blocking load. The 192-px SPRITE_TARGET_PLAYER and 96-px SPRITE_TARGET_ENEMY are chosen so enemy sprites can be downsampled aggressively (enemies are small on screen). Atlas-side bake then upscales the cached image into a 128-px scratch canvas before outline baking.
  • Cache safety check in getEnemySprite (!img.complete || img.naturalWidth <= 0) is defense against the case where downsampleSprite returned an image whose source hadn’t fully decoded. Returning null lets the renderer poly-fallback without a crash.
  • Module state (_cache, _loaded) is module-scoped and never reset. Reloads require a full page refresh.