Report-only system alignment page

Weapon-Loadout Customization — System Audit & Go-Forward Model

An audit of how the Deployment Plan First prototype customizes infantry/team weapon loadouts. The system grew through two parallel registries plus a third doubling mechanism, with no conflict detection and no cross-axis validation. This page inventories the current system, documents every confirmed conflict, inconsistency, and gap, then compares three candidate go-forward architectures and recommends one — gated for your approval. No prototype js/ changes are proposed in this pass.

Alignment date2026-06-26
Alignment statusreview
Product pathunthinkable
Topicweapon-loadout customization
Prototypeprototypes/unthinkable/tactical-battle/deployment-plan-first/
Scopereport-only; implementation deferred to a post-approval pass

1. Overview / Why this audit

The trigger was a visible symptom: the Sappers card could render a garbled "Demo/WP" munition bundle in its Explosives slot. Tracing it revealed a deeper structural problem — the loadout customization system has overlapping lanes, inconsistent trade-off rules, and silent collisions between the mechanisms that customize a unit.

Today a card's weapon loadout is customized through two parallel registries that were grown ad-hoc, merged by a blind shallow last-writer-wins spread with no conflict detection:

The two override tables merge through applyWeaponOverrides (js/model.js:844), applied one after the other in detailForItem (js/model.js:867–868). When a configuration and an equipment option both write the same slot, the equipment value silently wins — but both cost/card deltas still sum, so the player pays twice for one visible slot.

Your go-forward vision (captured for this audit): every slot customizable, each focused on its own lane; adding a weapon trades off a rifleman (and removing one returns it); Special is a flex slot that can hold equipment or "double-dip" into an extra non-rifle weapon. Section 6 maps this to a candidate model and recommends it.

All findings below are confirmed against live source: js/data.js, js/model.js, js/weapons.js, js/popover.js. Every claim cites file:line.

2. Current system inventory

2.1 The three customization mechanisms

MechanismSourceLane / what it touchesBalance leversNotes
unitLoadouts
configurations
data.js:628–685rifle / smg / lmg / at / grenade_demo; vehicles also use layout keys (main_gun_ap, vehicle_mg)costDelta, cardDeltaFirst entry per card is the default (0/0). A variant lists only the slots it changes via overrides.
unitEquipment
Special-slot kit
data.js:689–721"Owns" special by convention only; several options also rewrite grenade_democostDelta, cardDeltaThe grenade_demo writes are the source of the same-slot collision (§3).
extraWeaponSlots
doubling mechanic
data.js:728–732Doubles whatever weapon a fillable slot holds into special as a second copycostDelta, cardDelta, removesRifleThe rifle bookkeeping (removesRifle) lives here, not in the override tables. lmg +10/−1 removes rifle; smg +10 removes rifle; at +10 does not.

Merge path: detailForItem applies the configuration override first, then the equipment override second (model.js:867–868) via applyWeaponOverrides (model.js:844). Cost/card deltas are summed independently in itemStats (model.js:827–828) — they do not know about override collisions.

2.2 Per-card slot matrix

Which mechanism touches which slot, for each customizable card. = configuration writes the slot; = equipment writes the slot; marks a slot written by both axes on the same card (collision risk).

Cardriflesmglmgatgrenade_demo
(Explosives)
specialConfig source
Rifle Sectiondata.js:638–642
Rifle Half-Sectiondata.js:643–647
Sappers collision⚠ ● ◆data.js:648–652
Motor Rifle Sectiondata.js:653–657
Vickers MG Teamdata.js:668–671
Recce Patroldata.js:658–662
Infantry Platoon Leaderdata.js:663–667
Fire Support Universal Carrierdata.js:680–684
PIAT Team / AT teamsextra-weapon doubling via extraWeaponSlots (at = no rifle removal)data.js:728–732

Sappers is the only card where the same slot (grenade_demo) is written by both the configuration axis (Assault Party, data.js:650) and the equipment axis (Bangalore / AT mines / WP, data.js:701/702/704). Recce Patrol and Rifle Half-Section have equipment write grenade_demo but their configurations do not, so no collision there today — though nothing prevents one.

3. Conflicts

Evidence matrix — each row is a confirmed conflict with its source, impact, and confidence.

ClaimSourceImpactConfidence
Same-slot collision (the core bug). Sappers Assault Party config sets grenade_demo while Bangalore / AT-mines / WP equipment also set grenade_demo. Both selected → equipment (applied second) silently clobbers the config value. config data.js:650; equip data.js:701/702/704; merge order model.js:867–868 One visible slot, two writers; the player loses the Assault Party's demolition value with no warning. High
Delta double-counting. Even though one slot value survives, both the config and equipment costDelta/cardDelta still sum. model.js:827–828 (optionCostDelta + equipmentCostDelta, summed independently) The player pays the points/scarcity cost twice for a single visible munition change. High
No cross-axis validation anywhere. Any configuration + equipment combination is allowed; conflicts are resolved only by override application order. candidateSelectionForAxis popover.js:658 (sets the axis value, no conflict check) The collision class above can recur on any future card; nothing structurally forbids two axes writing one slot. High
Grid click-axis mis-routing. Cells hard-code special → equipment and everything else → configuration. The Explosives cell routes to the configuration picker even when equipment placed its contents. popover.js:589 (byKey: { special: "equipment" }) Clicking the Explosives slot opens the wrong lane's menu when equipment owns what's shown there — confusing edit affordance. High

4. Inconsistencies

ClaimSourceImpactConfidence
"Add a PIAT" has non-uniform side-effects. Usually at:{PIAT,1} only, but Recce PIAT Ambush also drops the LMG, and Panzerschreck (captured) also carries cardDelta:-1. No single "add a launcher" rule. data.js:640 (PIAT-only), data.js:661 (Recce drops LMG), data.js:641 (Panzerschreck cardDelta) The same player intent ("add a launcher") produces different bookkeeping per card; hard to reason about or extend. High
Trade-off rule is non-uniform. SMG/LMG gunners cost a rifleman; an AT launcher crewman does not. extraWeaponSlots data.js:729–731 (removesRifle true/true/false) Directly relevant to your vision: the "adding a weapon trades a rifleman" rule already has an undocumented exception. High
Mixed override namespaces in one registry. Infantry use canonical slot keys; vehicles use layout keys. infantry data.js:650; vehicle main_gun_ap data.js:674, vehicle_mg data.js:678 One registry, two key vocabularies — every reader/merger must handle both paths (model.js:846–855). High
Default-entry convention drift. Some cards default to literal "Standard" (special:"-"); others default to a named kit (Sappers "Engineer tools", Recce "Binoculars"); Staghound's loadout has no default entry at all. "Standard" data.js:691; Sappers data.js:700; Recce data.js:711; Staghound data.js:677–678 No single rule for "what is the unmodified card" — inconsistent baseline across cards. Medium
Ability detection is brittle string matching. Substring/exact-string tests with a "special-wins" short-circuit: equipping AT mines surfaces "Lay Mines" but suppresses "Plant Demolition" even though "Demo charges" is still in the Explosives slot. popover.js:413–426 (special wins; /demo charges/i & /smoke/i regex tests) Abilities can silently disappear or mismatch the displayed munitions; tightly coupled to exact munition strings. High

5. Gaps

ClaimSourceImpactConfidence
Non-foot weapon removals are invisible. The vehicle layout branch omits emptied cells, so a config that removes a vehicle weapon (Staghound "Hull MG deleted") produces no --removed diff highlight. Foot units keep the empty cell. weapons.js:226–233 (cells skipped when name === "-"); config data.js:678 A deletion the player chose is shown as "nothing there" rather than "removed" — the trade-off is not legible. High
Slots without a lane menu. rifle is never directly customizable (only flexes implicitly via gunner trade-offs); special-as-extra-weapon (your "double-dip") has no representation in the picker today. extraWeaponSlots data.js:728–732 (doubling exists but is not a per-slot lane); selection axes popover.js:585–591 The model you want (every slot a focused lane; Special as flex) is not expressible in the current structure. Medium
Stale weapon-short-name data. "Breach charges / No.77 WP" is emitted by no card; the "Demo/WP" short name collapses a 3-item bundle ("Demo charges / No.36M / No.77 WP") and reads as a fictional munition. weaponShortNames data.js:268 ("Demo/WP"), data.js:269 ("Breach/WP") Dead/misleading data; the "Demo/WP" face label is the visible symptom that opened this audit. High

6. Recommended go-forward model

Three candidate architectures, compared at the principles+structure altitude. The concrete data shape/schema for the recommended model is intentionally deferred to a post-approval pass.

ModelIdeaFixes collisions?EffortVerdict
A — Guardrail the status quo Keep both registries; add collision detection, forbid two axes writing one slot, fix the grenade_demo overlaps, the delta double-counting, and the click-axis routing. By patch, not by construction — the overlap can still be re-introduced. Lowest Keeps two systems and their conceptual overlap; treats symptoms.
B — Unified per-slot "lane" registry recommended One slotOptions table keyed by slot; each slot offers mutually-exclusive options within its own lane (rifle, smg, lmg, at, explosives, special). Rifle count derived automatically from gunner trade-offs. Special is a flex lane — equipment or an extra non-rifle weapon. Collapses unitLoadouts + unitEquipment + extraWeaponSlots into one. Yes — by construction. One writer per slot means same-slot collisions cannot exist. Moderate (rewrite data.js + merge/picker logic) Matches your vision directly; eliminates the bug class instead of guarding it.
C — Point-buy / independent toggles Each slot fully independent with a derived manpower budget; maximal flexibility. Yes, but with the largest new UX surface to keep coherent. Highest Most powerful, likely over-built for the card-face altitude.

6.1 Why Model B

Model B is recommended because it eliminates the entire same-slot collision class by construction (one writer per slot), collapses three mechanisms into one legible table, and is the direct expression of the go-forward vision — every slot a focused lane, with Special as the one flex slot. It is a moderate rewrite, not a research project (unlike C), and it removes the overlap rather than guarding it (unlike A).

6.2 Lane rules (principles, not schema)

Per-slot lanes

Each slot (rifle, smg, lmg, at, explosives, special) presents a menu of mutually-exclusive options drawn only from its own lane. Selecting an option in one lane cannot rewrite another lane's slot. This is what makes the collision impossible.

Rifle bookkeeping is derived, not authored

The rifle count is computed from the chosen gunner/launcher weapons rather than hand-written per option. Adding a gunner consumes a rifleman; removing one returns it — automatically, in one place, instead of the scattered per-option rifle:{...} rewrites and the separate removesRifle flag today.

Special is the flex lane

The Special slot is the single slot that can hold either a piece of equipment (smoke, mines, binoculars, demo) or a "double-dip" extra non-rifle weapon (a second LMG/SMG/launcher). It is the only lane with two kinds of content, and that flexibility is explicit rather than emergent from three colliding mechanisms.

6.3 Trade-off rule recommendation

Three options were considered: (a) uniform — every added gunner/launcher consumes a rifleman; (b) per-weapon — each weapon type sets its own rule (today's implicit state); (c) no trade-off — weapons are pure adds.

Recommendation: a uniform rule — "each added gunner/launcher weapon consumes a rifleman; removing one returns a rifleman" — as the default for consistency and legibility, with AT teams as the single, documented exception, where the loadout intent is a dedicated crew rather than a squad upgrade. One uniform rule, one principled carve-out, both stated explicitly rather than left implicit as today (where the at: removesRifle:false exception is unexplained in data.js:731).

7. Approval Gates

Answer the gates below, then use Compile Responses at the bottom to produce a YAML packet. This pass is report-only; an approved Model B becomes a separate post-approval implementation pass.

1. Adopt Model B (unified per-slot lane registry) as the go-forward architecture?
2. Adopt the uniform-with-AT-exception trade-off rule?
3. Confirm Special-as-flex (equipment OR an extra non-rifle weapon in the Special slot)?
4. Confirm this pass is report-only — defer all prototype js/ implementation to a post-approval pass?

Compile Responses

Answer any gate questions and/or add section feedback, then compile YAML for the agent. Complete approval requires all required gates answered with no unresolved negative feedback.