diff --git a/context/combat-damage.md b/context/combat-damage.md index bb88e99..869e2a2 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -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 diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index fddcf08..832ab50 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -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.