From fa0b825b6065e623a99dd96db8a2750c35b6d10b Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 13 Jul 2026 20:01:44 -0500 Subject: [PATCH] Combat: FIX missile direct damage 6x over-application -- damage once per salvo (task #62) User live regression: missiles killed a mech in ~2 shots ("takes way more than 2 normally"). Root cause -- DamageZone::TakeDamage (arcade @0041e4e0 == WinTesla DAMAGE.cpp:379) is `damageLevel += amount*scale` and IGNORES burstCount. So the arcade's ONE cluster Missile per trigger lands its damageAmount EXACTLY ONCE; the missileCount/burstCount split is cosmetic for zone damage (only gyro-bounce + splash-falloff read burstCount). The port re-expresses that one cluster as N flying rounds and was damaging on EVERY round = ~missileCount x too lethal on the direct hit (mirrors the splash over-application fixed in 145a69f). Fix (mislanch.cpp FireWeapon): only the salvo-LEAD round (i==0) carries damage (damageData.damageAmount) AND the cluster splash; the other N-1 rounds are VISUAL (damage 0) -- the tracer ripple, no extra damage. AC unaffected (one round/shot, not a cluster). Verified (BT_DMG_LOG [dmghit] trace): per-hit missile damage = clean 5.83, burst 1, once. Missiles-ONLY (BT_AF_MISSILE now independent of BT_AUTOFIRE), single enemy, 45s: 8 damaging hits, top zone reaches only 0.18, NO death (was death in ~3 pulls) -- "way more than 2" restored. Earlier test deaths were BT_AUTOFIRE firing laser+PPC+missiles combined, not missiles alone. KB: combat-damage.md documents burstCount-ignored + the N-round cluster trap (direct + splash both once-per-salvo). Co-Authored-By: Claude Opus 4.8 (1M context) --- context/combat-damage.md | 48 ++++++++++++++++++++------------- game/reconstructed/mech4.cpp | 8 +++--- game/reconstructed/mechdmg.cpp | 7 +++++ game/reconstructed/mislanch.cpp | 18 ++++++++----- 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/context/combat-damage.md b/context/combat-damage.md index 3dc0310..2959dee 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -125,9 +125,13 @@ Three-layer story, measured live + decomp-verified: 1. **The armor economy is POINTS**: every zone streams `damageScale[5]` — 5 cells indexed by `damageType` DIRECTLY (Collision/Ballistic/Explosive/Laser/Energy; task #60 correction — NOT an "even/odd slot" parity), each destructive type's cell ≈ `1/armorPoints` (armor 50–140). - `damageLevel += amount × damageScale[type]`, 1.0 = destroyed — engine `DAMAGE.cpp:379`, called - from `mechdmg.cpp:427`; the `[zone-armor]` dump, BT_DMG_LOG. Weapons author point-scale amounts - (laser 11.77 ≈ 10 torso hits). ✓ + `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. ✓ 2. **StaticBounce prices rams with the AUTHORED moverMass ≈ 1.3e6 units** → ~600×v² ≈ 59,000 points for a 10 m/s ram (measured: 59221@9.94, 398@0.81; `[collide-tx]` now logs mass/e). The binary DISPATCHES it raw (@part_012:15324-15358) — but in pod MP it landed on the local @@ -471,22 +475,28 @@ destroyed/zoneless mechs, applies the `dist>radius` gate + the burst falloff, de shooter's msgmgr (cross-pod) or `Dispatch`. Hooked at BOTH detonation paths (world-impact + contact). **⚠ THE SALVO-LEAD FIX (task #62 bug, found 2026-07-13 by live regression) — the N-round trap.** -The arcade fires ONE cluster Missile per trigger (burstCount=missileCount) → ONE SplashDamage event -with `baseBurst = missileCount`, floored at 1 ONCE. The port re-expresses that cluster as N flying -`BTProjectile` rounds (visual tracers). Firing splash PER ROUND (each `baseBurst=1`, each floored at -1) applied the floor N times = **~`missileCount`× too much splash** — a clustered bystander died in -~2 salvos (user-reported "missiles suddenly lethal"; direct hits were fine — a single enemy survived -24). Fix: `BTProjectile.splashBurst` tags ONLY the salvo-lead round (`FireWeapon` passes `nmiss` on -`i==0`, `0` on the rest); splash fires once per salvo with `baseBurst = missileCount`. Replicant -mirror rounds carry `0`/damage-0 (master delivers splash cross-pod — no double). **Lesson:** a cluster -weapon re-expressed as N rounds must apply per-CLUSTER effects (splash, and note the direct hit's -zone-concentration is likewise SPREAD in the port vs the arcade's single-zone cluster — a separate, -pre-existing under-concentration) once per salvo, not once per round. -**Verified:** radius resolves to 30 for missiles / 0 for AC+Emitters; near-miss dist=15 → 1 burst; -live rig (2 clustered mechs, 45s): bystander takes 6 splash events (1/salvo, `baseBurst=6`) and -SURVIVES (was ~2 shots pre-fix); primary dies to direct hits in ~3 trigger pulls (36 missiles, == -single-enemy TTK); AC does not splash; no crash. Env `BT_SPLASH_LOG`, `BT_SPLASH_TEST` (synthetic -near-miss), `BT_AF_MISSILE` (missile autofire), `BT_SPAWN_ENEMY=2` (clustered bystander rig). +**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 +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 +(`mislanch.cpp` FireWeapon): only the salvo-LEAD round (`i==0`) carries damage (`damageData. +damageAmount`) AND the cluster splash (`splashBurst = nmiss`); the other N-1 rounds are VISUAL +(damage 0, `splashBurst 0`) — the ripple of tracers. `BTProjectile.splashBurst` gates the two splash +hooks; the direct-hit block already skips `p.damage<=0`. Replicant mirror rounds carry 0 (master +delivers cross-pod — no double). **Lesson:** a cluster weapon re-expressed as N rounds must apply ALL +zone effects (direct damage AND splash) ONCE per salvo, not once per round — the arcade's single +Missile is one damage event, `burstCount` notwithstanding. +**Verified [T2]:** per-hit missile damage = clean 5.83, burst 1, ONCE (`[dmghit]` trace); radius 30 +for missiles / 0 for AC+Emitters. Missiles-ONLY, single enemy, 45s: 8 damaging hits, top zone reaches +only 0.18, **NO death** (was death in ~3 pulls pre-fix) — "way more than 2 shots" restored. AC (one +round/shot, not a cluster) unaffected. Env `BT_SPLASH_LOG`, `BT_DMG_LOG` (per-hit `[dmghit]` + +zone-armor), `BT_AF_MISSILE` (missile-only autofire, independent of BT_AUTOFIRE), `BT_SPAWN_ENEMY=N` +(N clustered dummies). ⚠ `BT_AUTOFIRE` fires ALL weapon groups (laser+PPC+AC) — use `BT_AF_MISSILE` +alone to isolate missile TTK. **Deferred [T3]:** the per-player enable sub-gate `Missile+0x360 = BTPlayer+0x264` (part_013.c:8757) — its decomp writers (4668/10512) read as a per-frame state toggle, not a clean config flag, and the port's `showDamageInflicted` label is itself a guess; port treats authored `SplashRadius>0` as the diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index cfd6346..5a91abf 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -3730,10 +3730,12 @@ void LBE4ControlsManager::ButtonJoystickThumbLow, // 0x46 LBE4ControlsManager::ButtonJoystickThumbHigh, // 0x47 }; - // DEV: BT_AF_MISSILE=1 also pulses the missile group (ThumbHigh) so a - // headless rig can exercise the launcher (splash verification, task #62). + // DEV: BT_AF_MISSILE=1 pulses the missile group (ThumbHigh). INDEPENDENT + // of BT_AUTOFIRE (which drives the main gun group) so a headless rig can + // exercise MISSILES ONLY -- isolates missile damage/splash from laser+PPC + // fire (task #62 TTK verification). static const int s_afMissile = getenv("BT_AF_MISSILE") ? 1 : 0; - const int autofireMsl = (s_afMissile && gBTDrive.fireForced && targetInArc) ? s_afPhase : 0; + const int autofireMsl = (s_afMissile && targetInArc) ? s_afPhase : 0; int want[4]; want[0] = ((gBTLaserKey && targetInArc) || autofire) ? 1 : 0; want[1] = (gBTPinkyKey && targetInArc) ? 1 : 0; // key '4' (was unmapped) diff --git a/game/reconstructed/mechdmg.cpp b/game/reconstructed/mechdmg.cpp index 99da2e1..93b8f6b 100644 --- a/game/reconstructed/mechdmg.cpp +++ b/game/reconstructed/mechdmg.cpp @@ -424,7 +424,14 @@ void } // ---- real zone: apply to armour/structure ---- + Scalar preLvl = damageLevel; DamageZone::TakeDamage(damage); // FUN_0041e4e0 + if (getenv("BT_DMG_LOG")) + DEBUG_STREAM << "[dmghit] mech=" << mech->GetEntityID() + << " zone=" << damageZoneIndex << " vital=" << (int)vitalDamageZone + << " type=" << (int)damage.damageType << " amt=" << damage.damageAmount + << " burst=" << damage.burstCount + << " lvl " << preLvl << "->" << damageLevel << "\n" << std::flush; Mech__DamageZone *parent = parentArtifactZone.GetCurrent(); // FUN_00417ab4(this+0x62) if (parent != 0) diff --git a/game/reconstructed/mislanch.cpp b/game/reconstructed/mislanch.cpp index 61569e4..e755281 100644 --- a/game/reconstructed/mislanch.cpp +++ b/game/reconstructed/mislanch.cpp @@ -305,15 +305,19 @@ void MissileLauncher::FireWeapon() // on the authored up-tilt, then the seeker loft + steering arc it onto the target. int nmiss = (missileCount > 0 && missileCount < 40) ? missileCount : 1; for (int i = 0; i < nmiss; ++i) - // SPLASH (task #62 fix): the whole salvo re-expresses ONE arcade cluster - // missile, which does ONE SplashDamage event with baseBurst = the cluster - // count. Tag ONLY the first round as the salvo lead (splash_burst = nmiss); - // the rest carry 0 so splash fires once per salvo, not once per visual round - // (which floored-at-1 N times = ~missileCount-x over-splash). - BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount, + // DAMAGE + SPLASH ONCE PER SALVO (task #62 fix): the arcade fires ONE + // cluster Missile per trigger, and its zone damage is applied EXACTLY ONCE + // -- DamageZone::TakeDamage (@0041e4e0) is `damageLevel += amount*scale` + // and IGNORES burstCount. The port draws that cluster as N visual rounds; + // damaging on EVERY round applied the hit N times = ~missileCount-x too + // lethal (the "2-shot kill" regression). Only the LEAD round (i==0) + // carries the missile's damageAmount + the cluster splash; the rest are + // VISUAL (damage 0) -- the ripple of tracers, no extra damage. + BTPushProjectile(muzzle, o, target, targetPos, speed, + (i == 0) ? damageData.damageAmount : 0.0f, &launchVelocity, 1, subsystemID /*messmgr explosion bundling at impact (task #7)*/, - (i == 0) ? nmiss : 0 /*salvo-lead cluster splash*/); + (i == 0) ? nmiss : 0 /*salvo-lead: cluster splash baseBurst*/); // Salvo replication (missile-visibility wave): bump the fire counter + stamp // the aim point; the extended update record carries both to peer nodes.