Back to Squad Table
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.

src/rules/units.ts
// 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`;
}