#95: REVERT the salvo-damage hack; deliver the cluster the way the binary does

Supersedes the mislanch change in efc3e9f, which multiplied the lead round by
missileCount.  That produced roughly the right average total but as ONE lump on
ONE zone, with no scatter and no variance -- not what the arcade does.

WHAT THE BINARY DOES (all byte-verified):
  * MissileLauncher ctor @004bcff0:  burstCount = missileCount;
                                     damageAmount /= missileCount
  * FireWeapon @004bcc60 spawns exactly ONE Missile -- no loop, missileCount is
    never read there.  The salvo IS one cluster round.
  * Missile::Perform rolls how many of the cluster connect right before it
    dispatches (part_013.c:10082):  b = Random(n) + n/4, clamped to n
    -- i.e. between a quarter of the salvo and all of it.
  * It then dispatches DIRECTLY at the struck entity (FUN_004be078:
    param_2->Dispatch(&msg)) -- NOT through the shooter's message manager.
  * Mech::TakeDamageMessageHandler @0x4a0439-0x4a04d8 applies the hit
    burstCount times, RE-ROLLING the struck zone per burst.

Our handler already implements that loop faithfully (mech.cpp, task #80) -- I
wrongly believed it was missing, because the KB asserts as [T1] that "burstCount
is cosmetic for zone damage".  That is half right: DamageZone::TakeDamage really
does ignore burstCount (verified @0041e4e0), but the HANDLER honours it by
calling that function repeatedly.  KB corrected separately.

The actual defect was that the projectile impact path hardcoded burstCount = 1,
and -- worse -- routed damage through the SubsystemMessageManager, whose
consolidation rebuilds the record from a stream carrying only {damageType,
damageAmount, subsystemID}.  DamageInformation has no burstCount field, so the
cluster count was DROPPED in transit no matter what the round carried.

So: dispatch projectile damage directly, as the binary does, carrying the rolled
burst.  This also retires the #84 double explosion architecturally -- the second
blast came from the manager's bundled explosion, and projectiles no longer go
through the manager at all.  The MarkRoundDetonated suppression added in efc3e9f
is therefore dead and is removed.

ALL-WEAPON-CLASS AUDIT (scratchpad/night8/allweap.{sh,py}), one run, all classes
firing at once:
  EXPLOSIVE (missile)  38 zone applications, bursts spread 1x2,2x6,3x9,4x4,5x5,
                       6x12 across ELEVEN zones -- the [n/4..n] roll plus the
                       per-burst zone re-roll, i.e. the cluster scattering
  ENERGY    (beam)      8 applications, burst 1  -- unchanged, no regression
  type4                 8 applications, burst 1  -- unchanged
  COLLISION             0 applications reached armour -- divert intact
Explosions: 10 projectile impacts produce no bundled blast; direct-fire still
queues its own (11 made).

NOT yet verified: cross-pod delivery.  Dispatch reroutes to a replicant on its
own (and the binary relies on exactly that), but the manager routing is gone, so
MP damage should get a two-node run before this ships.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-01 04:42:15 -05:00
co-authored by Claude Opus 5
parent efc3e9ff1a
commit 3b7c19c232
6 changed files with 144 additions and 119 deletions
+1 -42
View File
@@ -164,8 +164,6 @@ SubsystemMessageManager::SubsystemMessageManager(
commonDamageInformation.damageZoneIndex = -1; // this[0x3A]
commonDamageInformation.impactPoint = Point3D::Identity; // this[0x3B] = DAT_004e0f80
selfDetonatedCount = 0; // port-only (issue #84)
Check_Fpu();
}
@@ -390,19 +388,7 @@ void
DEBUG_STREAM << "[boom-q] subsysID=" << info->subsystemID
<< " weapon=" << (firingWeapon ? firingWeapon->GetName() : "<none>")
<< " explosionID=" << explosionID << std::endl;
//
// 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
if (explosionID != ResourceDescription::NullResourceID
&& !weaponExplosions.Find(explosionID)) // chain+0xFC, slot 0xC
{
ResourceIDPlug *plug = new ResourceIDPlug(explosionID);
@@ -447,7 +433,6 @@ 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
}
@@ -528,32 +513,6 @@ 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