Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
3.8 kB · 102 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103/** * 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<string, string | null>();
/** Names waiting for the next flush, and the callers waiting on them. */let queued = new Set<string>();let flushing: Promise<void> | 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<void> { 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<string, string> | 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<string | null> { 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);}