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.
// Rule data for the rules engine. This is the single place to check numbers
// against your rulebook and correct them. Values marked VERIFY are the
// author's best understanding and have not been checked line by line.
//
// Rule text is not reproduced here; each entry names the rule area so you can
// look it up.
import type { Terrain } from '../scenarios.js';
export type InfantryKind = 'squad' | 'half' | 'crew' | 'leader';
export const RULES = {
movement: {
/** Movement Factors per Movement Phase (movement chapter, infantry). */
infantryMF: { squad: 4, half: 4, crew: 4, leader: 6 } as Record<InfantryKind, number>,
/** Extra MF when a good-order leader moves with infantry as one stack the whole time. */
leaderBonusMF: 2,
/** Double Time: extra MF; the unit becomes CX (exhausted). */
doubleTimeMF: 1,
/** Road bonus: extra MF when every hex of the phase's movement is along a road. */
roadBonusMF: 1,
/** Cost multiplier for entering a hex at a higher level (upslope). VERIFY */
upslopeMultiplier: 2,
/** A unit that has not yet moved may always move one hex using all its MF. */
minimumMove: true,
},
/** Cost of terrain for infantry entering the hex, in MF. null = prohibited. */
terrainMF: {
open: 1,
grain: 1.5, // in season. VERIFY
orchard: 1,
brush: 2,
woods: 2,
'wood-bldg': 2,
'stone-bldg': 2,
marsh: 2, // VERIFY
water: null, // deep water: no infantry entry without a bridge/ford
} as Record<Terrain, number | null>,
/** Cost of terrain when entering along a connected road. */
roadMF: 1,
/** Extra MF to cross a hexside feature. VERIFY */
hexsideMF: { hedge: 1, wall: 1 },
/** Rout Phase: MF for broken infantry. VERIFY */
routMF: { squad: 4, half: 4, crew: 4, leader: 6 } as Record<InfantryKind, number>,
stacking: {
/** Max squad-equivalents per side per hex. */
squadEquivalents: 3,
/** Max single-man counters (leaders) per side per hex. VERIFY */
leaders: 4,
weight: { squad: 1, half: 0.5, crew: 0.5, leader: 0 } as Record<InfantryKind, number>,
},
/** Portage: what each unit carries without penalty, and support-weapon weights. */
portage: {
capacity: { squad: 3, half: 2, crew: 2, leader: 1 } as Record<InfantryKind, number>,
/** Weapon PP by catalogue suffix (e.g. "ge-mmg" → mmg). VERIFY all. */
weapon: { lmg: 1, mmg: 4, hmg: 5, mtr: 3, dc: 1, ft: 2, pf: 0, baz: 1, atr: 2 } as Record<string, number>,
/** MF lost per PP carried beyond capacity. */
penaltyPerPP: 1,
},
/** Board edge each side may enter from, per scenario (see scenarios.ts `entry`). */
} as const;
export const RULE_NOTES = [
'All costs, tables and unit values come from src/rules/data.ts. The IFT, close combat table, weapon stats and anything marked VERIFY there should be checked against your charts.',
'Automated: phase sequence, movement, line of sight, Prep/Defensive/Advancing Fire on the IFT, morale and pin checks, casualty reduction, cowering, weapon breakdown, rally, repair and close combat.',
'Not yet automated: Defensive First Fire during movement, fire groups spanning several hexes, rate of fire, ELR, leader loss checks, wounds, heat of battle, rout requirements, concealment, mortars and other special weapons, vehicles and guns.',
];
// ---------------------------------------------------------------------------
// Fire, morale, rally and close combat. EVERYTHING below is VERIFY: written
// from memory, not copied from a chart. Check it against your IFT, CCT and
// unit counters and correct it here.
// ---------------------------------------------------------------------------
export type IftResult = string; // "2KIA", "K/3", "2MC", "NMC", "PTC"
export const FIRE = {
/** IFT firepower columns, lowest first. */
columns: [1, 2, 4, 6, 8, 12, 16, 20, 24, 30, 36],
/** Result for each column by Final DR: index 0 is "≤0", 1 is DR 1, ... Past the end: no effect. VERIFY */
ift: {
1: ['1KIA', 'K/1', '1MC', 'NMC', 'PTC'],
2: ['1KIA', 'K/1', '2MC', '1MC', 'NMC', 'PTC'],
4: ['1KIA', 'K/2', '2MC', '1MC', 'NMC', 'NMC', 'PTC'],
6: ['2KIA', '1KIA', 'K/2', '2MC', '1MC', 'NMC', 'PTC', 'PTC'],
8: ['2KIA', '1KIA', 'K/3', '3MC', '2MC', '1MC', 'NMC', 'PTC', 'PTC'],
12: ['3KIA', '2KIA', '1KIA', 'K/3', '3MC', '2MC', '1MC', 'NMC', 'PTC'],
16: ['3KIA', '2KIA', '1KIA', 'K/4', '4MC', '3MC', '2MC', '1MC', 'NMC', 'PTC'],
20: ['4KIA', '3KIA', '2KIA', '1KIA', 'K/4', '4MC', '3MC', '2MC', '1MC', 'NMC', 'PTC'],
24: ['4KIA', '3KIA', '2KIA', '1KIA', 'K/4', '4MC', '4MC', '3MC', '2MC', '1MC', 'NMC', 'PTC'],
30: ['5KIA', '4KIA', '3KIA', '2KIA', '1KIA', 'K/4', '4MC', '3MC', '2MC', '1MC', 'NMC', 'PTC'],
36: ['6KIA', '5KIA', '4KIA', '3KIA', '2KIA', '1KIA', 'K/4', '4MC', '3MC', '2MC', 'NMC', 'PTC'],
} as Record<number, IftResult[]>,
/** Terrain Effects Modifier of the target hex. */
tem: { open: 0, grain: 0, orchard: 0, brush: 1, woods: 1, 'wood-bldg': 2, 'stone-bldg': 3, marsh: 0, water: 0 } as Record<Terrain, number>,
/** TEM of a wall/hedge on the target's hexside facing the firer (does not add to hex TEM; the higher applies). */
hexsideTem: { hedge: 1, wall: 2 },
/** Hindrance DRM per hex the LOS passes through (not the firer's or target's hex). */
hindrance: { grain: 1, orchard: 1, brush: 1 } as Partial<Record<Terrain, number>>,
/** Smoke in any hex of the LOS, including firer and target. */
smokeHindrance: 2,
/** A total hindrance this large blocks LOS. */
maxHindrance: 6,
/** Height above the ground level of terrain that blocks LOS. */
obstacleHeight: { woods: 1, 'wood-bldg': 1, 'stone-bldg': 1 } as Partial<Record<Terrain, number>>,
/** Range: beyond normal range up to this multiple is long range (FP × longRangeFP). */
longRangeMultiple: 2,
longRangeFP: 0.5,
/** Point Blank Fire (adjacent) and Triple PBF (same hex) FP multipliers. */
pbfFP: 2,
tpbfFP: 3,
advancingFireFP: 0.5,
pinnedFP: 0.5,
/** DRM when the firing group includes a CX unit. */
cxDRM: 1,
/** Cowering: original DR doubles without a leader directing shifts the column left. */
coweringShift: 1,
/** Squads with these stat lines are inexperienced: cower two columns. */
inexperienced: ['4-2-6'],
/** A squad may fire one support weapon and its own FP; half-squads and crews only the weapon. */
squadKeepsInherentWithSW: true,
halfSquadKeepsInherentWithSW: false,
/** Support weapon fire stats by catalogue key: firepower, normal range, breakdown number. */
weapons: {
'ge-lmg': { fp: 3, range: 8, b: 12 },
'ge-mmg': { fp: 5, range: 12, b: 12 },
'ge-hmg': { fp: 7, range: 16, b: 12 },
'us-lmg': { fp: 2, range: 6, b: 11 },
'us-mmg': { fp: 4, range: 10, b: 11 },
'us-hmg': { fp: 5, range: 15, b: 11 },
'ru-lmg': { fp: 2, range: 6, b: 11 },
'ru-mmg': { fp: 4, range: 10, b: 11 },
'ru-hmg': { fp: 6, range: 16, b: 11 },
} as Record<string, { fp: number; range: number; b: number }>,
/** All survivors of a KIA result break; already-broken survivors suffer casualty reduction. */
kiaSurvivorsBreak: true,
};
export const MORALE = {
/** An original 12 on a morale check also causes casualty reduction. */
twelveCasualty: true,
/** A broken unit that fails a morale check suffers casualty reduction. */
brokenFailReduces: true,
/** Rally needs a good-order leader in the hex (leaders may self-rally). */
rallyNeedsLeader: true,
selfRallyDRM: 1,
/** Rally DRM for broken units in woods or buildings. */
rallyTerrainDRM: -1,
/** Desperation Morale: rally DRM when an enemy unit is adjacent or in the hex. */
desperationDRM: 4,
/** An original 12 on a rally attempt causes casualty reduction. */
rallyTwelveCasualty: true,
/** Support weapon repair in the Rally Phase: dr ≤ repair fixes it; dr = 6 eliminates it. */
repair: 1,
repairLoss: 6,
};
/** Half-squad stat line after casualty reduction, keyed by full squad stat line. */
export const HALF_SQUAD: Record<string, string> = {
'5-4-8': '2-3-8',
'4-6-7': '2-4-7',
'4-4-7': '2-2-7',
'7-4-7': '3-3-7',
'6-6-6': '3-3-6',
'6-2-8': '3-2-8',
'4-2-6': '2-2-6',
};
export const CLOSE_COMBAT = {
/** Odds columns (attacker : defender) and the kill number for each. */
odds: [
{ ratio: 1 / 8, label: '1:8', kill: 1 },
{ ratio: 1 / 6, label: '1:6', kill: 2 },
{ ratio: 1 / 4, label: '1:4', kill: 3 },
{ ratio: 1 / 3, label: '1:3', kill: 4 },
{ ratio: 1 / 2, label: '1:2', kill: 5 },
{ ratio: 1, label: '1:1', kill: 6 },
{ ratio: 3 / 2, label: '3:2', kill: 7 },
{ ratio: 2, label: '2:1', kill: 8 },
{ ratio: 3, label: '3:1', kill: 9 },
{ ratio: 4, label: '4:1', kill: 10 },
{ ratio: 6, label: '6:1', kill: 11 },
{ ratio: 8, label: '8:1', kill: 12 },
{ ratio: 10, label: '10:1', kill: 13 },
],
/** Final DR below the kill number eliminates; equal causes casualty reduction. */
equalReduces: true,
/** Close-combat strength of a leader. */
leaderFP: 1,
/** Broken units add their FP to the defence. */
brokenDefend: true,
};
