/** * What a machine weighs and costs, for any screen that lists units. * * The table is generated — `scripts/unit-stats.py` collects it from what real * MegaMek reported about the daily scenarios — and it holds only what is true * of a design wherever it turns up: weight, price, introduction year. Battle * value is not here and cannot be, because BV 2.0 multiplies by the crew's * gunnery and piloting, so the same machine is worth one figure with one pilot * in it and another with a different one. Per-unit BV rides on the scenario * instead: a daily carries it per unit in daily.json, and the hand-written * catalog carries none at all yet. * * Which means every caller has to cope with not knowing. `statsFor` returns * null rather than zeroes, and a force card shows a dash where a figure would * go. Nine of the thirty-four machines the catalog fields have never appeared * in a daily and so have no entry; that is a gap in the data, and drawing it as * a gap is the honest rendering of it. */ import unitsJson from "./unit-stats.json"; /** What one design weighs, costs and dates from. */ export type UnitStats = { /** Tons. Halves exist — a Savannah Master is 5, a light hover can be 3.5. */ tons: number; /** C-bills, as MegaMek prices it. */ cost: number; /** The year the design was introduced. */ year: number; }; const UNITS: Record = unitsJson.units; /** * The name the table is keyed by. * * A scenario file writes `Drillson Heavy Hover Tank (Standard)`; MegaMek's own * `getShortName()` drops the `(Standard)` model, so that is the spelling the * report came back with and the spelling the table holds. Four units in the * dailies differ this way. Only the trailing marker is stripped — `Hellhound * (Conjurer) (Standard)` is reported as `Hellhound (Conjurer)`, and the part * that is a real second name for the design has to survive. */ function keyFor(designation: string): string { return designation.replace(/ \(Standard\)$/, ""); } /** What the table knows about a machine, or null when it has never seen it. */ export function statsFor(designation: string): UnitStats | null { return UNITS[keyFor(designation)] ?? null; } /** * A force's totals, and whether they are the whole story. * * `complete` is false when any unit in the force is missing from the table, so * a caller can mark the sum as a floor rather than print a total that quietly * leaves machines out. A partial total is worth showing — most of a force is * still most of a force — but not worth showing as if it were exact. */ export function forceTotals(designations: readonly string[]): { tons: number; cost: number; complete: boolean; } { let tons = 0; let cost = 0; let complete = true; for (const designation of designations) { const stats = statsFor(designation); if (!stats) { complete = false; continue; } tons += stats.tons; cost += stats.cost; } return { tons, cost, complete }; } /** Tons, with the half shown only when there is one: `5`, `3.5`. */ export function tonsText(tons: number): string { return Number.isInteger(tons) ? String(tons) : tons.toFixed(1); } /** * A price at a glance: `8.2M`, `915K`. Full precision is noise on a card that * is showing eight of them, and the figure is there to compare forces with. */ export function costText(cost: number): string { if (cost >= 1_000_000) return `${(cost / 1_000_000).toFixed(1)}M`; if (cost >= 1_000) return `${Math.round(cost / 1_000)}K`; return String(cost); }