/** * Which picture a design is drawn with, for the screens that draw a few. * * Two ways to the same answer, in this order: * * 1. `/api/units/art`, which resolves the names asked for out of the index * the API already has in memory. A few hundred bytes. * 2. The index itself, fetched and kept the way the force builder fetches * it — 300KB gzipped, but shared with every other screen on the page and * cached by the browser, so a second reader pays nothing. * * The fast path is what keeps the front page's daily card from pulling the * whole library to draw six units. The fallback is what keeps it drawing at * all when the API is unreachable, and it is the same data answering the same * question rather than a second source of truth. * * This replaced `scenario/unit-art.ts`, a generated map of the 61 designs the * scenario catalog names. The index carries all 8,456, so the map was a copy * of a column somebody had to remember to regenerate. */ import { fetchUnitArt } from "./api"; import { library } from "./library"; import { unitIndexUrl, unitSprite } from "./megamek"; import { candidates, loadUnits } from "./units"; /** Resolved paths, relative to `data/images/units/`. Null means "asked, and * this library has never heard of it" — cached so it is asked once. */ const known = new Map(); /** Names waiting for the next flush, and the callers waiting on them. */ let queued = new Set(); let flushing: Promise | null = null; /** * One request per tick rather than one per unit. * * A force card paints its machines in a loop, so without this a lance is * four requests and Bug Company is twelve. Everything asked for before the * microtask runs goes in one query. */ function flush(): Promise { flushing ??= Promise.resolve().then(async () => { const names = [...queued]; queued = new Set(); flushing = null; if (!names.length) return; const wanted = new Set(names.flatMap(candidates)); const lib = await library(); let found: Record | null = null; // An API that loaded no index answers this endpoint with an empty object // rather than an error, so asking it would look like an answer and every // card would draw nothing. `read` is what tells the two apart. if (lib.read) { try { found = await fetchUnitArt([...wanted]); } catch { found = null; } } if (!found) { // The API is not the only copy: the index is public and the force // builder already reads it. Slower and much larger, but it is the same // answer, and a page that can reach the assets bucket can still draw. try { const index = await loadUnits(unitIndexUrl(lib.helm, lib.megamek)); found = {}; for (const unit of index.units) { if (unit.sprite && wanted.has(unit.name)) found[unit.name] = unit.sprite; } } catch { // Neither reachable. Leave the names unresolved rather than // remembering a failure: the next caller tries again. return; } } const art = found; for (const name of names) { const hit = candidates(name).find((c) => art[c]); known.set(name, (hit ? art[hit] : null) ?? null); } }); return flushing; } /** * The address of one design's picture, or null where this library has no * entry for it — every caller draws something else in that case rather than * a broken image. */ export async function spriteUrlFor(name: string): Promise { if (!known.has(name)) { queued.add(name); await flush(); } const sprite = known.get(name); if (!sprite) return null; const { megamek } = await library(); return unitSprite(sprite, megamek); }