Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
5.7 kB · 150 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151/** * The scenario preview: every force drawn as its units, on hexes, in the * camo it will actually fight in — instead of a line of unit names. * * The rendering is the camo editor's, reused whole: `applyCamo` is the port * of MegaMek's own compositor, the hex behind each unit is the board tile a * match draws, and the sprites are MegaMek's real unit art. So what this * panel shows is what the lobby will show, not an artist's impression. * * Camo arrives per force through a callback, because whose camo a force * wears is the challenge screen's state: the player's picked camo on their * own slot, a rolled one on the AI's. A slot whose camo is null draws in * bare sprite grey, which is also true — that is what an unpainted force * looks like. */
import type { Scenario } from "../api";import { applyCamo, over } from "../camo/mech";import { paintWhenReady, tile } from "../megamek-art";import { el } from "../dom";import { spriteUrlFor } from "../unit-art";
// The neutral board tile from arena's skin, the same one the camo editor// previews against: it is what a lobby actually looks like.import battlespaceUrl from "../camo/backgrounds/battlespace.png?url";
// --- unit art ---------------------------------------------------------------//// Real MegaMek unit sprites, served from the assets bucket rather than// committed here: which picture a design gets is helm's index's answer, and// `unit-art.ts` is what asks. This module used to hold a generated map of the// 61 designs the scenario catalog names; the index carries all 8,456, so a// unit the catalog has never fielded now draws as itself too.//// Same terms as before, CC BY-NC-SA 4.0, and the attribution is in the repo// README with the rest of the MegaMek art. What changed is where the bytes// live, not whose they are.
// The caption's short name, and the variant beside it. Defined in// designation.ts because the daily card wants them without this module's// sprite map; re-exported here so lobby.ts's force cards keep their import.export { chassisOf, modelOf } from "./designation";import { chassisOf } from "./designation";
/** * One unit's art in the given camo (null for bare sprite grey), with nothing * behind it — null when the sprite map has never heard of the unit. * * Split out of paintUnitIcon for the daily card, which lays one continuous * hex field and then stands units on it: a unit that carried its own tile * would paste a tile's worth of hexes back over that field at whatever * offset it stands, and the field's lines would visibly break. */export async function paintUnitSprite( unit: string, camo: ImageData | null,): Promise<ImageData | null> { const spriteUrl = await spriteUrlFor(unit); if (!spriteUrl) return null; const sprite = await tile(spriteUrl); return camo ? applyCamo(sprite, camo) : sprite;}
/** * One unit, painted on its board tile in the given camo (null for bare * sprite grey — an unclaimed or someone-else's force). The compositing step * `update` below drives per hex, pulled out so lobby.ts's force cards can * paint the same units small, in the same paint, without a second * implementation of "sprite on tile in camo". */export async function paintUnitIcon( unit: string, camo: ImageData | null,): Promise<ImageData> { const bg = await tile(battlespaceUrl); const painted = await paintUnitSprite(unit, camo); return painted ? over(bg, painted) : bg;}
export type ScenarioPreview = { node: HTMLElement; /** * Redraws for a scenario. `describe` names who holds a slot ("You", * "AI", "@handle"); `camoFor` resolves the camo that force fights in, * null for none. Async work is sequenced: a stale redraw that resolves * late never paints over a newer one. */ update( scenario: Scenario | null, describe: (slot: string) => string, camoFor: (slot: string) => Promise<ImageData | null>, ): void;};
export function scenarioPreview(): ScenarioPreview { const node = el("div", { className: "scenario-preview" }); let seq = 0;
function update( scenario: Scenario | null, describe: (slot: string) => string, camoFor: (slot: string) => Promise<ImageData | null>, ): void { const drawing = ++seq; node.replaceChildren(); if (!scenario) return;
for (const slot of scenario.slots) { const hexes = el("div", { className: "force-hexes" }); const force = el("div", { className: "force" }, [ el("p", { className: "force-title" }, [ el("strong", { textContent: slot.name }), el("span", { className: "hint", textContent: ` — ${describe(slot.name)}`, }), ]), hexes, ]); node.append(force);
for (const { name: unit } of slot.units) { const canvas = el("canvas", { className: "force-unit" }); canvas.width = 84; canvas.height = 72; canvas.title = unit; const cell = el("figure", { className: "force-cell" }, [ canvas, el("figcaption", { className: "hint force-unit-name", textContent: chassisOf(unit), }), ]); hexes.append(cell);
void paintWhenReady( canvas, async () => paintUnitIcon(unit, await camoFor(slot.name)), () => drawing !== seq, ).catch((error: unknown) => { // A hex that cannot draw keeps its placeholder; the caption still // names the unit, so nothing is lost but the picture. console.warn("scenario preview: could not draw", unit, error); }); } } }
return { node, update };}