#95/#84: missile salvos deliver their FULL authored damage, and stop double-exploding

THE SALVO DAMAGE (#95).  Players measured an LRM 15 landing "3ish points".  The
logs agreed: [projectile] IMPACT damage=3.33333 (Oracle, LRM15) and 3.5 (Rajel,
LRM10).  Those are right PER MISSILE -- 50/15 and 35/10 -- and the bench shows
why the salvo still under-delivers: each launcher pushes N rounds of which
exactly ONE carries damage.

Two individually-correct changes composed into an N-fold shortfall:

  * The ctor does what the binary's MissileLauncher ctor does (@0x3ac/@0x3d4):
        damageData.burstCount    = missileCount;
        damageData.damageAmount /= missileCount;
    The record holds the PER-MISSILE amount plus the count; the arcade
    reconstitutes amount x burstCount when it applies the hit.
  * Task #62 then correctly stopped the port applying the hit once per visual
    round (that was ~missileCount-x too lethal) by damaging only the lead round
    -- but handed it the already-divided amount.

Our DamageZone::TakeDamage is `damageLevel += amount * scale` and drops
burstCount, so the salvo delivered amount/missileCount.  Since the port collapses
the cluster to one damaging round, multiply the count back in there.  Bench: an
SRM6 salvo now lands amt=35 (the authored total) taking a zone 0 -> 0.556, where
it previously landed 5.83.

THE DOUBLE EXPLOSION (#84).  Oracle: "missile appear to register hit explosions
twice, once where target was and again where the target is."  There are two
spawn sites: BTSpawnRoundDetonation at the round's own impact point, and the
message manager's bundled explosion at the CONSOLIDATED point a frame later.
The duplicate was known and thought harmless -- "among a rippled volley it is
invisible" -- which held only while a salvo landed N detonations.  A projectile
now marks its weapon (MarkRoundDetonated) and the consolidation skips queueing a
second blast for it; direct-fire weapons never mark, so lasers/AC keep the
bundled explosion they rely on.  Bench: 4 missile impacts -> 4 SKIPPED, while 11
direct-fire hits still queue normally.

SWEPT CONTACT (#84 tail).  Contact was a 10-unit sphere sampled only at the END
of each step.  With the authored thruster live (#84) field rounds arrive at
v=955 -- a ~16 unit step at 60fps, larger than the radius -- so samples can
straddle the target.  (Pre-#84 rounds flew ~100-300 = a 1.7-5 unit step and could
never skip it: the velocity fix exposed this, it did not cause it.)  Now tests
the whole segment travelled and bursts at the point of NEAREST APPROACH, which
also stops the detonation being flung past the target at speed.

RETRACTION: I posted tunnelling as the leading explanation for the lost salvo.
The bench disproves it -- zero fizzles, and the "missing" rounds are the dmg=0
visual rounds of the cluster, which never registered damage by design.  The
sweep is kept as speed-independent robustness, not as the #95 fix.

Bench: scratchpad/night8/salvo.sh (BT_PROJ_LOG + BT_FIRE_LOG).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-07-31 23:59:44 -05:00
co-authored by Claude Opus 5
parent 839b040619
commit efc3e9ff1a
7 changed files with 233 additions and 6 deletions
+42 -1
View File
@@ -164,6 +164,8 @@ SubsystemMessageManager::SubsystemMessageManager(
commonDamageInformation.damageZoneIndex = -1; // this[0x3A]
commonDamageInformation.impactPoint = Point3D::Identity; // this[0x3B] = DAT_004e0f80
selfDetonatedCount = 0; // port-only (issue #84)
Check_Fpu();
}
@@ -388,7 +390,19 @@ void
DEBUG_STREAM << "[boom-q] subsysID=" << info->subsystemID
<< " weapon=" << (firingWeapon ? firingWeapon->GetName() : "<none>")
<< " explosionID=" << explosionID << std::endl;
if (explosionID != ResourceDescription::NullResourceID
//
// issue #84: skip the bundled explosion for a weapon whose round
// already detonated itself at its own impact point. Queueing it
// too puts a second blast at the CONSOLIDATED point a frame later
// -- the "explosion where the target was, and again where it is".
//
if (DidRoundDetonate(info->subsystemID))
{
if (getenv("BT_FIRE_LOG"))
DEBUG_STREAM << "[boom-q] subsysID=" << info->subsystemID
<< " SKIPPED (round detonated itself)" << std::endl;
}
else if (explosionID != ResourceDescription::NullResourceID
&& !weaponExplosions.Find(explosionID)) // chain+0xFC, slot 0xC
{
ResourceIDPlug *plug = new ResourceIDPlug(explosionID);
@@ -433,6 +447,7 @@ void
commonDamageInformation.entityHit = 0; // this[0x39]
commonDamageInformation.damageZoneIndex = -1; // this[0x3A]
commonDamageInformation.impactPoint = Point3D::Identity; // this[0x3B]
selfDetonatedCount = 0; // port-only (issue #84): marks are per-flush
}
@@ -513,6 +528,32 @@ ResourceDescription::ResourceID
return ResourceDescription::NullResourceID;
}
//
// PORT-ONLY (issue #84) -- see the header note. A projectile that spawned its
// own round detonation marks its weapon here so the consolidation does not queue
// a SECOND, differently-placed explosion for the same hit.
//
void
SubsystemMessageManager::MarkRoundDetonated(int subsystem_id)
{
Check(this);
if (subsystem_id < 0 || DidRoundDetonate(subsystem_id))
return;
if (selfDetonatedCount >= kMaxSelfDetonated)
return; // full: fall back to the bundled blast
selfDetonated[selfDetonatedCount++] = subsystem_id;
}
Logical
SubsystemMessageManager::DidRoundDetonate(int subsystem_id) const
{
Check(this);
for (int i = 0; i < selfDetonatedCount; ++i)
if (selfDetonated[i] == subsystem_id)
return True;
return False;
}
//
// SubmitExplosion -- the real spawn (task #7 tail). The binary posts a
// Registry::MakeEntityMessage (id 3, class 0x31 Explosion, flags 0x100) at