PURPOSE
Per-hull asset loader for the v4 ships pipeline. Loads an unlit diffuse PNG plus an optional _points.json bundle for each hull and exposes them to the renderer through a small synchronous cache. Assets are authored by the sprite workbench and exported to public/ships-v4/ via the dev export endpoint; the runtime loads them by hull class and renders them through the shared sprite batch.
OWNS
- The module-level v4 asset cache (
_assetCache: hull name →ShipV4Assets). - The in-flight request map (
_pending) that collapses concurrent preload calls for the same hull into one fetch. - The memoized manifest promise (
_manifest) that lists every exported v4 ship. - The
ShipV4AssetsandPointsJsonruntime types and theShipV4ManifestEntryshape. - Path construction for diffuse and points URLs (including
encodeURIComponentof the hull key).
READS FROM
public/ships-v4/<hull>.png— unlit diffuse sprite per hull (HTMLImageElement load).public/ships-v4/<hull>_points.json— per-hull points-of-interest JSON, parsed asPointsJson.public/ships-v4/manifest.json— prebuilt manifest of exported hulls ({ entries: ShipV4ManifestEntry[] }), tried first byloadShipsV4Manifest./__dev/ships-v4-manifest— dev-server middleware fallback used when the prebuilt manifest is absent.
PUSHES TO
- The render path, which calls
hasShipV4to branch between legacy and v4 draw, then reads bundles throughgetShipV4. - UI image tags, which call
getShipV4SpritePath(hull)to get a public URL for an<img>source. - Boot warm-up, which calls
preloadAllShipV4()so the first render of any hull does not pop in.
DOES NOT
- Mutate game state.
preloadShipV4is pure asset I/O. - Throw on missing assets. Missing files resolve to
null; consumers must handle the null case directly. - Normalize, lower-case, or otherwise transform hull keys. Keys are the exact filename without extension, case-sensitive, with spaces preserved (e.g.
"Big Bertha", not"big bertha"), because Vite static serving is case-sensitive in prod. - Provide a legacy
/ships/fallback. The legacy player loader was removed. - Decode or use the contents of the
pointsmap; the schema is reserved (Record<string, never>) and only the version and sprite dimensions are typed. - Render anything itself. Drawing belongs to the sprite batch and renderer modules.
Signals
None. The loader has no event bus, callback list, or subscriber API. Consumers poll synchronously via getShipV4 / hasShipV4 after awaiting (or fire-and-forgetting) preloadShipV4.
Entry points
preloadShipV4(hull)— start loading a hull’s diffuse and points in parallel; idempotent and concurrency-safe. Returns the cached bundle, the in-flight promise, or a fresh fetch promise. Resolves tonullif the diffuse 404s.preloadAllShipV4()— resolve the manifest then preload every listed hull. Used at boot. Resolves once every hull has either loaded or failed.loadShipsV4Manifest()— fetch the manifest, prebuilt first then dev middleware, and memoize the result. Returns[]if neither source is reachable.getShipV4(hull)— synchronous accessor for an already-loaded bundle. Returnsnullif not loaded or never existed.hasShipV4(hull)— cheap synchronous presence check used by the render-path branch.getShipV4SpritePath(hull)— public URL string for the hull’s diffuse sprite; used by UI<img>tags.clearShipV4Cache()— clear asset cache, pending map, and memoized manifest. Tests and hot-reload only.
Pattern notes
- Cache + in-flight map is the standard duplicate-request collapser: check
_assetCachefirst, then_pending, otherwise start the fetch, register it in_pending, and remove it in afinallyblock once settled. loadImageandloadJsonboth swallow errors and resolve tonullrather than throwing, matching the contract that missing assets are not exceptional.- A bundle is committed to
_assetCacheonly when the diffuse loaded. The points JSON is allowed to benullindependently, so a hull with a diffuse but no points still caches as a validShipV4Assets. - The manifest loader chains two
tryblocks rather than usingPromise.any, so the dev endpoint is only hit when the prebuilt fetch throws or returns a non-okresponse. - All URL construction goes through
encodeURIComponenton the hull key so spaces and punctuation in authored hull names survive as valid URLs. - Constants (
ASSET_ROOT,DIFFUSE_EXT,POINTS_SUFFIX,MANIFEST_URL,DEV_MANIFEST_URL) are module-private; consumers go through the exported functions.