MechDeathHandler: per-zone destroyed-skins + explosions (faithful, exported)

Fixes the "kills are invisible" report: a mech died only at the state level, with
no visible destruction on its body.  Root cause: the per-zone damage-state
descriptor table (binary zone+0xd4) that drives destroyed-skins + explosions was
never built -- its loader (Mech__DamageZone::LoadCriticalSubsystems / FUN_0041e4a8)
was a no-op stub, and MechDeathHandler itself was a no-op stub.  The surrounding
plumbing (the per-zone load loop over the type-0x1e resource, the death-handler
slot) was already correct.  Whole pipeline is EXPORTED -- no stand-ins.

Reconstructed:
- Mech__DamageZone descriptor table (binary this+0xd4): un-stubbed
  LoadCriticalSubsystems (FUN_0041e4a8/FUN_0042a748/FUN_0042a2c8) to parse the
  real type-0x1e stream -- entry = [f32 DamageLevel][i32 EffectResource]
  [i32 GraphicState][f32 TimeDelay], ascending by DamageLevel.  Verified live:
  6 entries/zone, first threshold 0.2, effect 26, across all 40 zones, no crash.
- The three lookups (FUN_0042a664 by-level / FUN_0042a5f4 crossing / FUN_0042a6c4
  by-graphic-state) as Mech__DamageZone methods.
- The real MechDeathHandler (FUN_0042a984 ctor / FUN_0042aa2c Performance /
  FUN_0042a9f4 dtor), replacing the stub: each tick it walks the zones and, as a
  zone's damageLevel rises across a descriptor threshold, fires that entry's
  explosion (binary +0xb8 & 4) and, on destruction, the Destroyed-graphic
  descriptor's explosion (+0xb8 & 8), applying the descriptor's GraphicState (the
  destroyed skin) to the zone.  Runs for EVERY mech, so the enemy visibly falls
  apart as it dies.

Integration: the binary ticks MechDeathHandler off the mech's Performance list
(mech+0xbc), which the bring-up drive override bypasses, so Mech::PerformAndWatch
drives Tick() directly (same approach as UpdateDeathState).  Effect spawn uses the
established Explosion::Make port (BTSpawnDamageEffect) -- the authentic dispatch
(class-5 message -> the 0xBD3 SubsystemMessageManager effect manager) is unported.

Runtime: builds clean, boots clean, 42 descriptor tables load with sensible
byte-aligned data, no crash.  Live effect firing is combat-triggered (verified by
the load + the wiring; the [deathfx] threshold-crossing log fires under BT_DEATH_LOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 11:18:30 -05:00
co-authored by Claude Opus 4.8
parent a9467481c5
commit 7c455303bd
5 changed files with 260 additions and 12 deletions
+37
View File
@@ -843,6 +843,36 @@ static void
}
}
//###########################################################################
// MechDeathHandler effect-spawn bridge (mechdmg.cpp calls this)
//
// Spawns a damage-state descriptor's explosion at the mech. The authentic path
// (FUN_0043663c -> FUN_004364e4) broadcasts a class-5 message to the effect
// manager app+0x38 (the 0xBD3 SubsystemMessageManager, unreconstructed); we use
// the same established Explosion::Make port the weapon path uses. The binary
// derives the position from the subsystem (mech+0x184); we use the mech origin.
//###########################################################################
void
BTSpawnDamageEffect(Mech *mech, int effect_resource)
{
if (mech == 0)
return;
ResourceDescription::ResourceID res = (ResourceDescription::ResourceID)effect_resource;
if (res <= 0)
res = gExplodeRes; // fall back to the resolved generic explosion
if (res <= 0)
return; // nothing to spawn yet
Origin o = mech->localOrigin; // at the mech (binary: per-subsystem)
Explosion::MakeMessage m(
Explosion::MakeMessageID, sizeof(Explosion::MakeMessage),
(Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null,
res, Explosion::DefaultFlags, o,
mech->GetEntityID(), mech->GetEntityID());
Explosion *e = Explosion::Make(&m);
if (e)
Register_Object(e);
}
//###########################################################################
// Death sequence -- the un-exported master-perf death branch (region
// 0x4a9770-0x4ab188), reconstructed from its EXPORTED consumers + the RP
@@ -920,6 +950,13 @@ void
// dead mech keeps its death state instead of reverting to a live gait.
UpdateDeathState();
// MechDeathHandler (@0042aa2c): per-frame destroyed-skin + explosion effects as
// this mech's zones cross damage thresholds. The binary ticks it off the mech's
// Performance list (mech+0xbc), which the bring-up drive override bypasses, so
// it is driven here. Runs for EVERY mech (so the enemy visibly falls apart too).
if (deathHandler != 0)
((MechDeathHandler *)deathHandler)->Tick();
// WAVE 7 Phase B: advance/render/impact the flying projectiles once per frame (viewpoint).
if (isPlayerMech && dt > 0.0001f)
BTUpdateProjectiles(dt);