--- id: ev title: Damage as a distribution, not a mean status: open dependsOn: [protocol] exitCriterion: > Fire allocation is chosen on P(threshold), and the calculator is tested against hand-computed cases. --- # ev At a to-hit of 11 a PPC has an expected 0.83 damage and an actual outcome of "nothing, 92% of the time". Overkill decisions need the distribution; the mean says a Locust needs two PPCs when it needs many more. (The figure was written here against a to-hit of 10, which 2d6 reaches one time in six, not one in twelve.) The calculator lives in `crates/sds-core/src/ev.rs`. Nothing calls it yet. - [x] Per-shot PMF over integer damage: `1 - P(hit)` at zero, the rest across the cluster table for the rack size, times per-missile damage - [x] Convolve shots for an allocation; memoise on `(rack, per-missile damage, to-hit, cluster modifier)` - [x] Query it for mean, p50, p90, `P(>=20)` (the piloting-roll cliff) - [x] Carry expected packet count alongside, for punch-versus-sandblast - [x] `P(kill)` and `P(breach)`, against the location the damage reaches rather than the target's total. `at_least(threshold)` is still the whole of the arithmetic; what was missing was not the threshold but the distribution to apply it to. `EvCache::location_allocation` carries the per-location compound distribution, and `hitloc` supplies the shares. See `plan/features.md` for the size of the error this removed - [ ] Feed AMS into the cluster modifier. The key has the field and the roll applies it; nothing on the wire says a target mounts AMS - [ ] Force-level greedy allocation against the concave value curve. Waits on force-level assignment machinery, which does not exist yet. Greedy on a submodular objective is within 1-1/e; any search over subsets is exponential - [x] **Declare high-concentration weapons first.** Declaration order is resolution order - `TWGameManager.handleAttacks` walks `getAttacksVector()` calling `handle`, with no sort or comparator in it - and the ladder already orders a declaration by descending damage per packet, which is "most concentrated first". That sort exists to choose which subsets to *offer*, so the property was incidental; it is now pinned by a test that fails if the ladder is reordered. The guarantee is over one declaration: the attacks vector interleaves both sides in arrival order, so a friendly gun opening a location for a friendly cluster weapon is reliable and an enemy's position in that vector is not. Ordering by `p_breach` rather than by packet size is a possible refinement and is not measured. ## `WeaponType.getDamage()` is a sentinel more often than it is a number Four weapon classes now, and each was found the same way - a figure that read zero or negative, traced to an accessor answering a sentinel rather than a damage: | class | what `getDamage()` gives | where the real figure lives | |---|---|---| | cluster (LRM, SRM) | `DAMAGE_BY_CLUSTER_TABLE` = -2 | the cluster table, by rack size | | variable | `DAMAGE_VARIABLE` | per weapon | | artillery | `DAMAGE_ARTILLERY` | `getRackSize()` | | conventional infantry | **-3 per weapon** | `ConvInfantry.getDamagePerTrooper()` x troopers | Measured for the last of those: a Clan Heavy Foot Point delivers **3.00** and a 25-trooper motorised platoon **8.50**, where summing `getDamage()` over their weapon lists gives **-15.0** for both. Nothing in the weapon list carries it, because a platoon's damage is a function of how many troopers it has. **The pattern is the finding.** `ClusterGrouping.resolvedDamage` is our reimplementation of MegaMek's combination rules and it accrues one special case per weapon class we get wrong. Every instance so far has been an accessor whose name promises a damage and whose return is a code, and every one was caught by a figure reading zero rather than by anything failing. - [ ] Carry a platoon's real damage on the wire, the way the incoming divisor is. `projected_damage` sums the weapon list and reads nought for every conventional infantry design, which makes `range_band_fit` nought for them too - see `plan/mounting.md`, where that is what the drop clamp needed