From 4480300485a01b7e010ec8a257e18765b004f763 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Fri, 21 Aug 2026 17:17:24 -0400 Subject: [PATCH] feat(unit-search)!: widen the browser's unit type, and flip the heat filter The index has published run_mp, omni, clan, config, source, rules_level and mul_id since it existed and the type declared none of them, so a screen could not read one without a cast. The breaking half is `heat`: the index carries dissipation less firing heat, so a positive figure meant a cool machine and `heat>=10` returned the opposite of what it asked for. It now filters on heat the sinks cannot shed, read through the one accessor the screen reads, so `heat<=0` is the design that can keep firing. `run`, `damage` and `range` join the keys, each a column that is now visible on a row. --- web/src/units.ts | 103 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 25 deletions(-) diff --git a/web/src/units.ts b/web/src/units.ts index 3e0ee5d..d729702 100644 --- a/web/src/units.ts +++ b/web/src/units.ts @@ -23,17 +23,33 @@ export interface Unit { model: string; /** `chassis (clanname) model`, MegaMek's display name. Not a key. */ name: string; + /** masterunitlist.info's record id, where the design has one. */ + mul_id: number | null; unit_type: string | null; + /** Biped, Quad, Tripod, LAM — the chassis arrangement. */ + config: string | null; tech_base: string | null; + rules_level: string | null; role: string | null; + /** The sourcebooks the design appears in, comma-separated. */ + source: string | null; year: number | null; mass: number | null; walk_mp: number | null; + /** + * MegaMek's own running figure, not walk rounded up by half. MASC, a + * supercharger and an industrial chassis all move it, and 481 designs in + * 0.51.0 run at something other than the arithmetic. + */ + run_mp: number | null; jump_mp: number | null; total_armor: number | null; bv: number | null; cost: number | null; weight_class: number | null; + /** An OmniMech, whose pods can be swapped between configurations. */ + omni: boolean | null; + clan: boolean | null; /** Relative to `data/images/units/`, for `unitSprite`. */ sprite: string | null; firepower: number | null; @@ -41,6 +57,24 @@ export interface Unit { heat_efficiency: number | null; } +/** + * The heat one alpha strike leaves behind: positive is heat the sinks cannot + * shed in the turn it was made. + * + * The index carries the difference the other way up, as dissipation less the + * heat of firing — helm calls it efficiency, and a positive figure there is a + * design that can keep firing. That is the wrong way round for a player, who + * is asking how much trouble one alpha strike buys and reads a bigger number + * as more of it. Flipped in one place, so the chip on a row and the `heat` + * filter cannot come to disagree about which way is bad. + * + * Weapon heat only. Walking, running and jumping all add to it, and none of + * them are in the index's figure. + */ +export function excessHeat(unit: Unit): number | null { + return unit.heat_efficiency === null ? null : -unit.heat_efficiency; +} + /** The file itself: what it describes, and what built it. */ export interface UnitIndex { megamek: string; @@ -259,31 +293,50 @@ export interface Query { * both. Answering that needs the introduction, extinction and reintroduction * dates the index does not carry, so the key is unclaimed rather than wrong. */ -const FIELDS: Record = +const FIELDS: Record< + string, { - year: { column: "year", kind: "number" }, - bv: { column: "bv", kind: "number" }, - mass: { column: "mass", kind: "number" }, - tons: { column: "mass", kind: "number" }, - tonnage: { column: "mass", kind: "number" }, - weight: { column: "mass", kind: "number" }, - walk: { column: "walk_mp", kind: "number" }, - jump: { column: "jump_mp", kind: "number" }, - armor: { column: "total_armor", kind: "number" }, - armour: { column: "total_armor", kind: "number" }, - fp: { column: "firepower", kind: "number" }, - firepower: { column: "firepower", kind: "number" }, - reach: { column: "max_range", kind: "number" }, - heat: { column: "heat_efficiency", kind: "number" }, - cost: { column: "cost", kind: "number" }, - class: { column: "weight_class", kind: "number" }, - chassis: { column: "chassis", kind: "text" }, - model: { column: "model", kind: "text" }, - name: { column: "name", kind: "text" }, - tech: { column: "tech_base", kind: "text" }, - role: { column: "role", kind: "text" }, - type: { column: "unit_type", kind: "text" }, - }; + column: keyof Unit; + kind: "number" | "text"; + /** + * Read this instead of the column, where what a player asks about is not + * what the index stores. Only `heat` needs it, and it needs it because + * the sign a screen shows and the sign a query takes have to be one + * thing. + */ + derive?: (unit: Unit) => number | null; + } +> = { + year: { column: "year", kind: "number" }, + bv: { column: "bv", kind: "number" }, + mass: { column: "mass", kind: "number" }, + tons: { column: "mass", kind: "number" }, + tonnage: { column: "mass", kind: "number" }, + weight: { column: "mass", kind: "number" }, + walk: { column: "walk_mp", kind: "number" }, + run: { column: "run_mp", kind: "number" }, + jump: { column: "jump_mp", kind: "number" }, + armor: { column: "total_armor", kind: "number" }, + armour: { column: "total_armor", kind: "number" }, + fp: { column: "firepower", kind: "number" }, + firepower: { column: "firepower", kind: "number" }, + damage: { column: "firepower", kind: "number" }, + reach: { column: "max_range", kind: "number" }, + range: { column: "max_range", kind: "number" }, + heat: { + column: "heat_efficiency", + kind: "number", + derive: excessHeat, + }, + cost: { column: "cost", kind: "number" }, + class: { column: "weight_class", kind: "number" }, + chassis: { column: "chassis", kind: "text" }, + model: { column: "model", kind: "text" }, + name: { column: "name", kind: "text" }, + tech: { column: "tech_base", kind: "text" }, + role: { column: "role", kind: "text" }, + type: { column: "unit_type", kind: "text" }, +}; /** Weight classes by the name a player uses rather than by MegaMek's code. */ const CLASSES: Record = { @@ -377,7 +430,7 @@ export function stringifyQuery(query: Query): string { export function matchesFilter(unit: Unit, filter: Filter): boolean { const field = FIELDS[filter.key]; if (!field) return true; - const held = unit[field.column]; + const held = field.derive ? field.derive(unit) : unit[field.column]; if (field.kind === "text") { const text = String(held ?? "").toLowerCase(); -- 2.51.2