Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
5.0 kB · 142 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143/** * A force being built, and what it is worth. * * The list itself is a plain array of picks; everything else here is * arithmetic over it, kept apart from the screen so the numbers can be * tested without a DOM. * * Battle value is the design's own, from the index. What a crew does to it — * a 3/4 pilot is worth more than a 4/5 one — is helm's rules rather than * arithmetic, and belongs on the wasm side when crews become editable. Until * then every unit is flown by a regular crew, which is the multiplier of 1.0 * the published figure already assumes. */
import type { Unit } from "./units";
/** One machine in the force. */export interface Pick { unit: Unit; /** Stable within a page, so a row can be removed without matching names. */ id: number;}
/** What a target constrains, and how hard. */export type TargetMode = "advisory" | "locked";
export interface Target { /** Total battle value the force is built to. */ battleValue: number | null; /** Nothing designed after this year. */ year: number | null; /** Total tonnage. */ tonnage: number | null; /** * Whether the targets filter what search offers, or only warn. * * Optional either way: a force with no target is a force somebody is * building for its own sake, which is most of them. */ mode: TargetMode;}
export const NO_TARGET: Target = { battleValue: null, year: null, tonnage: null, mode: "advisory",};
/** The force's totals, and how they sit against the target. */export interface Standing { battleValue: number; tonnage: number; units: number; /** Battle value over the target, or 0 when there is none or it fits. */ overBy: number; /** Battle value left under the target; null when there is no target. */ remaining: number | null; /** Designs later than the year target, which is a warning and not a bar. */ tooLate: Pick[];}
export function standing(picks: Pick[], target: Target): Standing { const battleValue = picks.reduce((sum, p) => sum + (p.unit.bv ?? 0), 0); const tonnage = picks.reduce((sum, p) => sum + (p.unit.mass ?? 0), 0); const cap = target.battleValue; return { battleValue, tonnage, units: picks.length, overBy: cap === null ? 0 : Math.max(0, battleValue - cap), remaining: cap === null ? null : cap - battleValue, tooLate: target.year === null ? [] : picks.filter((p) => (p.unit.year ?? 0) > target.year!), };}
/** * How wide a unit's segment is on the strip, as a percentage. * * Share of the target where there is one, so the bar reads against the * budget and the gap at the end is what is left. Over the target, the whole * force is scaled to fill the bar instead and the target line moves in to * mark where it sits — the alternative is segments running off the end, * which says the same thing less clearly. */export function shares(picks: Pick[], target: Target): number[] { const total = picks.reduce((sum, p) => sum + (p.unit.bv ?? 0), 0); const against = Math.max(target.battleValue ?? total, total, 1); return picks.map((p) => ((p.unit.bv ?? 0) / against) * 100);}
/** Where the target sits on a strip that is scaled to the force. */export function targetLine(picks: Pick[], target: Target): number | null { const cap = target.battleValue; if (cap === null) return null; const total = picks.reduce((sum, p) => sum + (p.unit.bv ?? 0), 0); return total > cap ? (cap / total) * 100 : null;}
/** * The target's constraints, written as query terms. * * A locked target is a filter like any other, so it is expressed in the same * language the box takes rather than as a second mechanism: the screen can * then show what the target is imposing, and a player can see that * `year<=3025` in their own query and the one the target added are the same * kind of thing. Battle value is the exception and stays in code — "what fits * in what is left" depends on the force, which no static term can say. */export function targetTerms(target: Target): string[] { if (target.mode !== "locked") return []; const terms: string[] = []; if (target.year !== null) terms.push(`year<=${target.year}`); return terms;}
/** * Whether a locked target would keep this design out of the results. * * Every target that is set, not only the year: a locked battle value means * "what still fits in what is left", which is the question a player with 260 * BV to spend is actually asking, and a locked tonnage the same. Advisory * targets filter nothing — that is the whole difference between the two. */export function withinTarget( unit: Unit, target: Target, now?: Standing,): boolean { if (target.mode !== "locked") return true; if (target.year !== null && (unit.year ?? 0) > target.year) return false; if (now && target.battleValue !== null) { if ((unit.bv ?? 0) > (now.remaining ?? 0)) return false; } if (now && target.tonnage !== null) { if (now.tonnage + (unit.mass ?? 0) > target.tonnage) return false; } return true;}