KB: correct the burstCount claim -- it is honoured by the CALLER, not cosmetic

The KB asserted as [T1] that "burstCount is cosmetic for zone damage".  Half of
that is byte-verified (DamageZone::TakeDamage @0041e4e0 really does ignore it);
the other half was an inference that inherited the [T1] tag and then justified
task #62's salvo-lead design -- which silently divided every missile salvo by its
missile count (gitea #95).

Corrected in both places it appeared (combat-damage.md:334 and :884): the CALLER,
Mech::TakeDamageMessageHandler @0x4a0423-0x4a04d8, applies TakeDamage burstCount
times and re-rolls the struck zone per burst.  burstCount = number of
applications; load-bearing for missile cluster count, splash falloff and the gyro
bounce.  Also records that the arcade Missile dispatches DIRECTLY at the victim
rather than through the message manager (whose consolidation drops burstCount).

New gotcha 24: a verified fact, over-generalised, becomes a wrong design premise.
Detection smell -- a "cosmetic/unused" claim about a field other code still
computes carefully.  Nobody spends instructions randomising a decorative value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-01 04:43:34 -05:00
co-authored by Claude Opus 5
parent 3b7c19c232
commit c23d06cbb5
2 changed files with 42 additions and 8 deletions
+23 -8
View File
@@ -332,10 +332,12 @@ Three-layer story, measured live + decomp-verified:
`damageLevel += amount × damageScale[type]`, 1.0 = destroyed — engine `DAMAGE.cpp:379` (arcade
`@0041e4e0`), called from `mechdmg.cpp:427`; the `[zone-armor]` + per-hit `[dmghit]` dumps,
BT_DMG_LOG. Weapons author point-scale amounts (laser 11.77 ≈ 10 torso hits). **`burstCount` is
NOT in this formula — zone damage IGNORES it** [T1]; one Damage message = one `amount×scale`
application regardless of burstCount (burstCount is read only by the gyro-bounce math + the
SplashDamage falloff). A cluster missile therefore lands its damageAmount ONCE — see the
SALVO-LEAD FIX below for why the port's N-round missiles must damage once per salvo. ✓
NOT in this formula — `TakeDamage` itself IGNORES it** [T1]. ⚠ But one Damage message is **NOT**
one application: `Mech::TakeDamageMessageHandler` calls `TakeDamage` **`burstCount` times**,
re-rolling the struck zone per burst (@0x4a0423-0x4a04d8; `mech.cpp`, task #80). `burstCount` =
number of applications, and it is load-bearing (missile cluster count, splash falloff, gyro
bounce). A cluster missile lands its damageAmount once **per connecting missile** — the count is
rolled at impact, `Random(n) + n/4` clamped to `n`. See the corrected SALVO note below. ✓
2. **StaticBounce prices rams at ~600×v² raw** (measured: 59221@9.94, 66239@9.59, 398@0.81;
`[collide-tx]` logs mass/e — mech moverMass is the tonnage-scale 60000-90000, NOT the old
"authored ≈1.3e6" mis-attribution; the ~24·v²·0.0005·mass factor is the head-on (vn·vp)
@@ -881,10 +883,23 @@ cross-pod for a replicant victim on its own.
**⚠ THE SALVO-LEAD FIX (task #62 bug, found 2026-07-13 by live regression) — the N-round trap.**
**KEY FACT [T1]: `DamageZone::TakeDamage` (arcade `@0041e4e0` == WinTesla `DAMAGE.cpp:379`) is
`damageLevel += damageAmount * damageScale[type]` and IGNORES `burstCount`.** So ONE arcade cluster
Missile per trigger (damageAmount = authored/missileCount, burstCount = missileCount) applies its hit
EXACTLY ONCE to a zone — `burstCount` is cosmetic for zone damage (only the gyro-bounce math
`gyro.cpp:834` and the SplashDamage falloff `damage.burstCount/dist^exp` read it). The port
`damageLevel += damageAmount * damageScale[type]` and IGNORES `burstCount`.**
⚠ **CORRECTION (2026-08-01, #95): do NOT generalise that into "burstCount is cosmetic for zone
damage" — the earlier wording here said exactly that and it is WRONG.** `TakeDamage` ignores it;
the **CALLER honours it**. `Mech::TakeDamageMessageHandler` (@0x4a0423-0x4a04d8, reconstructed in
`mech.cpp` under task #80) loops `burstCount` times, calling `TakeDamage` once per burst and
**re-rolling the struck zone each iteration** (@0x4a04b9) — so a burst SPRAYS across zones.
`burstCount` is therefore "number of applications", and it is load-bearing for missiles (cluster
count), splash (distance falloff) and the gyro bounce alike.
So ONE arcade cluster Missile per trigger (damageAmount = authored/missileCount, burstCount =
missileCount) delivers its salvo through that loop, not in a single application. **How many of the
cluster connect is ROLLED at impact** in `Missile::Perform` right before dispatch
(part_013.c:10082): `b = Random(n) + n/4`, clamped to `n` — between a quarter of the salvo and all
of it. **And the arcade Missile dispatches DIRECTLY at the struck entity** (`FUN_004be078`:
`param_2->Dispatch(&msg)`) — it does NOT route through the shooter's `SubsystemMessageManager`,
whose consolidation would drop `burstCount` (`DamageInformation` carries only damageType +
subsystemID). Routing a projectile through the manager therefore silently deletes the cluster
count — that was the #95 bug, and it also produced the #84 double explosion. The port
re-expresses that ONE cluster as N flying `BTProjectile` rounds (visual tracers), and task #62
damaged + splashed on EVERY round → **~`missileCount`× too lethal on BOTH the direct hit and the
splash** (user-reported "missiles kill in 2 shots" — a mech that should take many salvos). Fix
+19
View File
@@ -755,3 +755,22 @@ equally covers the ramp-baked-texture path and pure-emissive batches.
**Detection smell:** a colour change that logs perfectly at the source and produces zero visible
difference. Diff two runs pixel-wise against a control region before believing a colour path
works — "the log says 0.1x" is not evidence that anything reached the screen.
## §24 — A verified fact, OVER-GENERALISED, becomes a wrong design premise
**(2026-08-01, gitea #95 "missiles land 3 points instead of 50".)** The KB carried this, tagged
**[T1]**: *"`DamageZone::TakeDamage` is `damageLevel += amount*scale` and IGNORES `burstCount` — so
`burstCount` is cosmetic for zone damage."* The first clause is **true and byte-verified**
(`@0041e4e0`). The second is an **inference** that was written down beside it, inherited the [T1]
tag, and then justified a design decision (task #62's salvo-lead model) that silently divided every
missile salvo by its missile count for months.
**What was actually true:** `TakeDamage` ignores `burstCount`; the **CALLER**
(`Mech::TakeDamageMessageHandler` @0x4a0423-0x4a04d8) honours it by calling `TakeDamage` that many
times, re-rolling the zone per burst. The consumer was one stack frame up — the same blind spot
shape as §21 (export-gap blindness), but with the evidence *present* and simply not followed
outward.
**Detection smell:** a "cosmetic"/"unused"/"vestigial" claim about a field that other code still
computes carefully. Here the binary randomises `burstCount` at impact (`Random(n) + n/4`) and the
splash code derives it from a distance falloff — nobody spends instructions rolling a decorative
value. **If a field is called dead, ask who still writes it, and why.**
**Rule:** tag the VERIFIED clause, not the paragraph. An inference sitting next to a [T1] fact is
still [T4] — split them, or the next reader (including you) will build on the guess.