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
+62 -8
View File
@@ -85,7 +85,14 @@ extern BTDriveInput gBTDrive;
//#############################################################################
// Reconstruction support: artifact constants, globals and helper stand-ins.
//
static const Scalar kDamageScale = 1.0f; // _DAT_004bafbc firing-physics scale (best-effort)
static const Scalar kDamageScale = 1.0e-7f; // _DAT_004bafbc -- an x87 80-bit constant
// (bytes bc427ae5d594bfd6e73f @0x4bafbc)
// = 1e-7 EXACTLY: it cancels the ctor's
// energyTotal x1e7, so at recommended
// charge damagePortion == the AUTHORED
// DamageAmount verbatim (task #8; the old
// 1.0f "best-effort" was the whole
// damage-economy mystery)
static const Vector3D DAT_004e0fa4(0.0f, 0.0f, 0.0f); // zero vector
static char DAT_00522524[4] = { 0 }; // default colour tag
static int PTR_LAB_00511e6c, DAT_00511e70, DAT_00511e74; // beam-keepalive message template
@@ -178,9 +185,27 @@ void
// E = 1/2 * currentLevel^2 * energyCoefficient
Scalar energy = 0.5f * currentLevel * currentLevel * energyCoefficient; // _DAT_004bafb8
damagePortion = damageFraction * energy; // 0x44c
heatPortion = energy - damagePortion; // 0x450
damagePortion = kDamageScale * damagePortion; // firing-physics scale (best-effort)
// THE AUTHENTIC PER-SHOT DAMAGE (task #8) -- the closed form of the
// binary's energy algebra (@004bb120 ctor x @004bace8 fire):
// damagePortion = dF x (0.5 V^2 EC) x 1e-7
// = authored DamageAmount x (charge / seekV[rec])^2
// (kDamageScale = _DAT_004bafbc = 1e-7 EXACTLY cancels the ctor's x1e7;
// derived here from the STORED damageFraction/energyTotal so the authored
// base never degrades, and the bring-up charge cycle -- calibrated to the
// degenerate EC=1 electrical model -- stays untouched. At full charge:
// the authored value verbatim; madcat ERLLaser=6, bhk1 PPC=12.)
{
Scalar vRec = seekVoltage[seekVoltageRecommendedIndex];
Scalar chargeRatio = (vRec > 0.0f) ? (currentLevel / vRec) : 1.0f;
damagePortion = (damageFraction * energyTotal * kDamageScale)
* chargeRatio * chargeRatio; // 0x44c
}
// Heat stays on the bring-up scale until the heat-calibration audit [T3]:
// the authentic heatPortion is heatCostToFire x 1e7 energy units (the
// missile path already feeds that scale raw); switching the emitters too
// is the heat audit's call, not the damage economy's.
heatPortion = energy - damageFraction * energy; // 0x450
// dump the heat portion into our INHERITED thermal accumulator if heatable/online.
// E5: pendingHeat (HeatSink @0x1C8) is the field the heat sim absorbs each frame
@@ -223,10 +248,20 @@ void
if (dist <= effectiveRange) // this+0x328
{
// The binary writes the impact point/delta/energy into the inherited Damage
// damageData (0x3C8/0x3B0/0x3AC); the recon's beamHitPoint/beamImpact/
// beamImpactScalar copies were dead (no readers) -> removed. Register the pip.
DrawWeaponPip(aimTransform); // FUN_004b9728
// THE AUTHENTIC DAMAGE SUBMISSION (task #8; binary @004bace8:7758-7764):
// fill the inherited Damage record -- amount = this shot's
// damagePortion (== authored DamageAmount at full charge),
// damageForce = target - muzzle (ALSO the gyro's directional
// hit-bounce source, retiring its random fallback), impactPoint =
// the target point -- then submit through SendDamageMessage
// (@004b9728) into the owner's SubsystemMessageManager.
damageData.damageAmount = damagePortion; // @0x3AC
damageData.damageForce.Subtract(targetPoint, muzzlePoint); // @0x3B0
damageData.impactPoint = targetPoint; // @0x3C8
damageData.burstCount = 1;
Entity *submitTarget = (owner != 0)
? *(Entity **)((char *)owner + 0x388) : 0; // mech target slot
SendDamageMessage(submitTarget); // FUN_004b9728
}
// stash the beam endpoint for replication (world hit point) + the beam's
@@ -275,6 +310,21 @@ void
// per-type keyboard split are retired -- weapons sharing a button now fire
// TOGETHER (the pod's authored groups: e.g. madcat Trigger = 4 weapons).
Logical fireEdge = CheckFireEdge(); // @004b9608
// task #8 diag: the discharge-chain probe (button->TriggerState->edge->FSM)
if (getenv("BT_FIRE_LOG"))
{
static int s_fp = 0;
if ((++s_fp % 97) == 0 || fireEdge) // 97 prime: rotates through all weapons
DEBUG_STREAM << "[fire-probe] " << GetName()
<< " owner=" << (void *)owner
<< " trigState=" << (float)fireImpulse
<< " edge=" << (int)fireEdge
<< " alarm=" << (int)weaponAlarm.GetState()
<< " level=" << (float)currentLevel
<< " tgt=" << (void *)((owner != 0)
? *(Entity **)((char *)owner + 0x388) : 0)
<< std::endl;
}
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
// hard failure: powered-off, faulted, or the OWNING MECH destroyed (@004baa88
@@ -791,6 +841,10 @@ Emitter::Emitter(
(void)energyRampTime;
}
// (task #8 note: energyCoefficient deliberately stays on the bring-up
// electrical model -- the charge cycle is calibrated to it; the AUTHENTIC
// damage algebra is applied as a closed form at fire time instead.)
// damageFraction = damageAmount / (damageAmount + heatCostToFire)
damageFraction = damageData.damageAmount /
(damageData.damageAmount + heatCostToFire); // 0x444