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.
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:
unitLoadouts— configurations (PIAT sections, assault parties, command groups).unitEquipment— Special-slot kit (smoke, mines, demo charges, binoculars).- plus
extraWeaponSlots— a separate "double-up a weapon" doubling mechanism that also encodes the rifleman trade-off rule.
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.
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
| Mechanism | Source | Lane / what it touches | Balance levers | Notes |
|---|---|---|---|---|
unitLoadoutsconfigurations | data.js:628–685 | rifle / smg / lmg / at / grenade_demo; vehicles also use layout keys (main_gun_ap, vehicle_mg) | costDelta, cardDelta | First entry per card is the default (0/0). A variant lists only the slots it changes via overrides. |
unitEquipmentSpecial-slot kit | data.js:689–721 | "Owns" special by convention only; several options also rewrite grenade_demo | costDelta, cardDelta | The grenade_demo writes are the source of the same-slot collision (§3). |
extraWeaponSlotsdoubling mechanic | data.js:728–732 | Doubles whatever weapon a fillable slot holds into special as a second copy | costDelta, cardDelta, removesRifle | The 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).
| Card | rifle | smg | lmg | at | grenade_demo (Explosives) | special | Config source |
|---|---|---|---|---|---|---|---|
| Rifle Section | ● | ● | ◆ | data.js:638–642 | |||
| Rifle Half-Section | ● | ● | ◆ | ◆ | data.js:643–647 | ||
| Sappers collision | ● | ● | ● | ● | ⚠ ● ◆ | ◆ | data.js:648–652 |
| Motor Rifle Section | ● | ● | ● | ◆ | data.js:653–657 | ||
| Vickers MG Team | ● | data.js:668–671 | |||||
| Recce Patrol | ● | ● | ● | ◆ | ◆ | data.js:658–662 | |
| Infantry Platoon Leader | ● | ● | ● | ◆ | data.js:663–667 | ||
| Fire Support Universal Carrier | ● | ● | ● | ● | ● | ● | data.js:680–684 |
| PIAT Team / AT teams | extra-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.
| Claim | Source | Impact | Confidence |
|---|---|---|---|
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
| Claim | Source | Impact | Confidence |
|---|---|---|---|
"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
| Claim | Source | Impact | Confidence |
|---|---|---|---|
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.
| Model | Idea | Fixes collisions? | Effort | Verdict |
|---|---|---|---|---|
| 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.
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.
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.