SOURCE / PINNED RELEASE
Made of little things.
Squad Table
- Release
- d36201f8ed4f…
- Author-recorded commit
- 7b9bb681d6ba…
- License
- LICENSE
- Author’s source reference
- nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-c043b0c0f1d
Archive hash verified: 8c0604b31f00cc5a…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Unit capabilities read from counters. Infantry stats come from the printed
// stat line ("4-6-7" = firepower-range-morale, "9-1" = leader morale and
// modifier), so relabelling a counter changes its values.
import type { Counter, TableState } from '../model.js';
import { FIRE, HALF_SQUAD } from './data.js';
import { roleOf, type Role } from './movement.js';
import { lift, put, relocate } from './ops.js';
export type Stats = { role: Role; fp: number; range: number; morale: number; leaderDRM: number; b?: number };
export function labelOf(c: Counter): string {
return c.label ?? '';
}
export function statsOf(c: Counter, printed: string): Stats | null {
const role = roleOf(c);
const label = c.label ?? printed;
if (role === 'squad' || role === 'half' || role === 'crew') {
const m = /^(\d+)-(\d+)-(\d+)/.exec(label);
if (!m) return null;
return { role, fp: Number(m[1]), range: Number(m[2]), morale: Number(m[3]), leaderDRM: 0 };
}
if (role === 'leader') {
const m = /^(\d+)-(\d+)/.exec(label);
if (!m) return null;
return { role, fp: 0, range: 0, morale: Number(m[1]), leaderDRM: -Number(m[2]) };
}
if (role === 'weapon') {
const base = FIRE.weapons[c.k];
const m = /(\d+)-(\d+)/.exec(label);
if (!base && !m) return null;
return {
role,
fp: m ? Number(m[1]) : base!.fp,
range: m ? Number(m[2]) : base!.range,
morale: 0,
leaderDRM: 0,
b: base?.b ?? 12,
};
}
return null;
}
export function isPersonnel(c: Counter): boolean {
return ['squad', 'half', 'crew', 'leader'].includes(roleOf(c));
}
/** Enemy personnel in a hex (the units a fire attack or close combat affects). */
export function personnelIn(state: TableState, hex: string, side: 'a' | 'b'): Counter[] {
return (state.stacks.get(hex) ?? [])
.map((id) => state.counters.get(id)!)
.filter((c) => c && c.side === side && isPersonnel(c));
}
export function clearStatus(c: Counter): void {
c.cx = undefined;
c.pin = undefined;
c.fired = undefined;
}
export function eliminate(state: TableState, c: Counter): void {
if (c.side !== 'a' && c.side !== 'b') {
lift(state, c);
state.counters.delete(c.id);
return;
}
clearStatus(c);
c.face = 0;
relocate(state, c, `elim-${c.side}`);
}
/**
* Casualty reduction: a squad becomes a half-squad (keeping broken/pin
* status); a half-squad, crew or leader is eliminated. Returns a description.
*/
export function casualtyReduce(state: TableState, c: Counter, printed: string): string {
const role = roleOf(c);
const label = c.label ?? printed;
if (role === 'squad' && (c.side === 'a' || c.side === 'b')) {
const nat = c.k.split('-')[0];
const half: Counter = {
id: `${c.id}h`,
k: `${nat}-hs`,
side: c.side,
face: c.face,
rot: 0,
label: HALF_SQUAD[label] ?? 'HS',
at: c.at,
pin: c.pin,
fired: c.fired,
cx: c.cx,
};
lift(state, c);
state.counters.delete(c.id);
put(state, half);
return `${label} reduced to half-squad ${half.label}`;
}
eliminate(state, c);
return `${label} eliminated`;
}
