Combat: THE AUTHENTIC DAMAGE ECONOMY -- authored per-weapon amounts through the real fire chain (task #8)

Three root causes, all fixed:
1. kDamageScale was 1.0 -- _DAT_004bafbc is an x87 float80 = 1e-7, cancelling
   the ctor's x1e7: damagePortion = authored DamageAmount x (charge/seekV)^2
   (closed form at fire time; madcat AC=25/LRM=50/ERLL=6, bhk1 PPC=12/SRM=35).
   The observed "0.25" was the degenerate EC=1 fallback, never authored data.
2. CheckFireEdge NaN latch: TriggerState carries ControlsButton INTS; the
   release value (-65) is a negative NaN.  The binary's x87 unordered compare
   read it as "released"; IEEE-correct float compares latched the edge
   detector shut after the first release -- the reason the emitter discharge
   chain NEVER fired in-game.  Fixed with bit-pattern sign compares.
3. The weapon-side submission LIVE: Emitter::FireWeapon fills damageData
   (amount + damageForce=target-muzzle [the gyro directional-bounce feed] +
   impact) -> MechWeapon::SendDamageMessage (@004b9728 real body) ->
   messmgr consolidation with per-weapon records + explosion bundling.
   The mech4 bring-up damage block + flat kShotDamage retired to diag hooks.
Bonus: LODReuseHysteresis 0.82 -> 0.33 (double misread).  Zone model
verified byte-exact (no change).  Solo end-to-end: 5-record volleys (2 PPC
+ 3 ERML with authored amounts), per-weapon zone granularity, explosions,
kill.  Heat stays bring-up scale pending the heat-calibration audit [T3].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 15:32:24 -05:00
co-authored by Claude Fable 5
parent 77190c93e5
commit fd055281a8
17 changed files with 654 additions and 75 deletions
+35 -46
View File
@@ -672,6 +672,13 @@ extern void BTPostKillScore(Entity *victim, Scalar damage); // KILL (+ MP death
#define MECH_TARGET_ENTITY(m) (*(Entity **)((char *)(m) + 0x388))
#define MECH_TARGET_SUBIDX(m) (*(int *)((char *)(m) + 0x38c))
// task #8 bridge: the weapon-side damage submission (mechweap.cpp) reads the
// owner's designated-zone slot; the raw target-slot block lives in this TU.
int BTMechTargetZone(void *mech)
{
return MECH_TARGET_SUBIDX(mech);
}
// Bring-up body-animation player (file scope so OnBodyAnimFinished can re-arm it).
// The engine AnimationInstance walks the mech's joint subsystem from a baked .ani
// clip in btl4.res; we advance it each moving frame so the legs CYCLE.
@@ -3042,7 +3049,19 @@ void
s_prevBtn[b] = want[b];
ControlsButton v = (ControlsButton)(want[b]
? (buttons[b] + 1) : -(buttons[b] + 1));
cm->buttonGroup[buttons[b]].Update(&v, modeMask);
cm->buttonGroup[buttons[b]].ForceUpdate(&v, modeMask); // diag: bypass the prev-diff gate
if (getenv("BT_FIRE_LOG"))
{
static int s_pushN = 0;
if ((++s_pushN % 60) == 1)
DEBUG_STREAM << "[push] btn=0x" << std::hex
<< buttons[b] << std::dec << " v=" << (int)v
<< " mode=0x" << std::hex << (int)modeMask
<< std::dec << " #" << s_pushN
<< " thisMech=" << (void *)this
<< " viewpoint=" << (void *)application->GetViewpointEntity()
<< std::endl;
}
}
}
}
@@ -3170,52 +3189,22 @@ void
// routes it to Mech__DamageZone::TakeDamage. Dispatch on a
// REPLICANT is rerouted by Entity::Dispatch to the owning
// master over the wire -- the cross-pod damage path.
if (((Mech *)victim)->damageZoneCount > 0)
// DAMAGE DELIVERY MOVED (task #8): the AUTHENTIC submitter is
// each firing weapon's own SendDamageMessage (@004b9728, called
// from Emitter::FireWeapon with the authored per-shot
// damagePortion + damageForce + impact) into the
// SubsystemMessageManager. This block keeps only the
// presentation roles: the boresight pick feeds the target
// slots the weapons read (mech+0x388/0x37C/0x38C), the [fire]
// beam-entry explosion visual above, and the shot log. The
// flat kShotDamage build + shooter-side score moved with the
// submission (mechweap.cpp).
if (getenv("BT_FIRE_LOG") && ((Mech *)victim)->damageZoneCount > 0)
{
Damage dmg;
dmg.damageType = Damage::ExplosiveDamageType;
dmg.damageAmount = kShotDamage;
dmg.burstCount = 1;
dmg.impactPoint = impact;
Entity::TakeDamageMessage take_damage(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
GetEntityID(), // inflicting = this (shooter)
-1, // UNAIMED -> receiver's cylinder resolves
dmg);
// AUTHENTIC DELIVERY (task #7 tail): submit through the
// SubsystemMessageManager -- per-frame consolidation into
// ONE TakeDamageStream (id 0x13) dispatched at the victim,
// whose T0 handler re-splits it (ENTITY.cpp:817).
// Replicant victims reroute cross-pod as before. [T3:
// the inflicting subsystemID is not threaded through this
// bring-up fire block -- the authentic submitter is the
// weapon's own SendDamageMessage @004b9728, pending the
// damage-economy reconciliation (authored 0.25-scale
// amounts vs the bring-up kShotDamage=12); until then the
// messmgr's weapon-explosion bundling stays dormant.]
if (messageManager != 0)
((SubsystemMessageManager *)messageManager)
->AddDamageMessage(victim, &take_damage);
else
victim->Dispatch(&take_damage);
// SCORE the damage (skip once the victim is dead).
if (!((Mech *)victim)->IsMechDestroyed())
BTPostDamageScore(victim, kShotDamage);
int zone = take_damage.damageZone;
if (zone >= 0 && zone < ((Mech *)victim)->damageZoneCount)
{
Scalar s = ((Mech *)victim)->Zone(zone)->damageLevel;
DEBUG_STREAM << "[damage] " << (void*)victim << " zone " << zone
<< "/" << ((Mech *)victim)->damageZoneCount
<< " structure=" << s
<< (((Mech *)victim)->GetInstance() == ReplicantInstance
? " (REPLICANT -> cross-pod)" : "")
<< "\n" << std::flush;
}
DEBUG_STREAM << "[damage] beam hit " << (void*)victim
<< (((Mech *)victim)->GetInstance() == ReplicantInstance
? " (REPLICANT -> cross-pod via the weapon submit)" : "")
<< "\n" << std::flush;
}
// DEATH effects fire at the VICTIM's own death transition
// (UpdateDeathState, task #42) -- MP-correct: the master runs it.