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
+64 -17
View File
@@ -58,6 +58,7 @@
//
#include <bt.hpp>
#include <messmgr.hpp> // SubsystemMessageManager (task #8 damage submission)
#pragma hdrstop
#if !defined(MECHWEAP_HPP)
@@ -376,13 +377,25 @@ Logical
// @004b9608 -- rising-edge detector on fireImpulse (threshold _DAT_004b9648 == 0.0f).
// Returns True the frame fireImpulse becomes positive while the previous sample
// was non-positive, then stores the current sample for next time.
// TODO: confirm exact role (discharge vs. recharge-ready edge).
//
// ⚠ BIT-PATTERN COMPARE (task #8 root-cause): TriggerState carries the raw
// ControlsButton INT bit-copied by the streamed direct mapping (+(button+1)
// press / -(button+1) release) -- the release value (e.g. -65 = 0xFFFFFFBF)
// reads as a NEGATIVE NaN in float terms. The binary's x87 compare (FNSTSW/
// SAHF + JBE: unordered sets CF|ZF => branch TAKEN) effectively treated the
// NaN as "released", so its edge re-armed; an IEEE-correct float compare
// makes NaN<=0 FALSE and the detector LATCHES SHUT after the first release
// (observed live: one startup edge, then never again). Compare the BIT
// PATTERNS as signed ints instead -- sign-correct for the button ints AND
// for genuine float values (0.0f = 0x0, positives > 0, negatives/NaNs < 0),
// reproducing the binary's effective behavior exactly.
//
Logical
MechWeapon::CheckFireEdge()
{
Logical edge =
(fireImpulse > 0.0f && previousFireImpulse <= 0.0f) ? True : False;
int current = *(int *)&fireImpulse;
int previous = *(int *)&previousFireImpulse;
Logical edge = (current > 0 && previous <= 0) ? True : False;
previousFireImpulse = fireImpulse;
return edge;
}
@@ -465,23 +478,57 @@ void
// routing through AddDamageMessage. This body stays dormant meanwhile.
//
void
MechWeapon::DrawWeaponPip(const LinearMatrix &transform)
MechWeapon::SendDamageMessage(Entity *target)
{
// @004b9728 -- register the targeting "pip" with the owning Mech's cockpit
// HUD manager. Skipped for non-pip weapons (pipSegment == -1). The HUD
// element record {priority 100, kind 0x12, enabled, team colour, pipPosition,
// pipExtendedRange, transform, pipSegment} is submitted to the cockpit HUD.
//
// CROSS-FAMILY (hud): the cockpit HUD manager AddElement entry point lives in
// the HUD module; the submission is wired there. We retain the gating and
// transform reference here.
if (useConfiguredPip) // this+0x3E0 (weapon draws a configured pip)
// @004b9728 -- package this weapon's just-fired damageData as an
// Entity::TakeDamageMessage and submit it to the owning mech's
// SubsystemMessageManager for per-frame consolidation (AddDamageMessage
// @0049b6d8) -- the authentic beam damage delivery (task #8; the caller
// Emitter::FireWeapon fills damageAmount/damageForce/impactPoint first).
//
// Binary gate (part_013.c:6746): a Mech victim with NO designated zone
// (owner+0x38C == -1) is SKIPPED. The port PASSES -1 through [T3
// divergence]: our Mech::TakeDamageMessageHandler cylinder-resolves the
// unaimed zone (STEP 6), preserving the verified aiming model.
//
Mech *mech = (Mech *)GetEntity(); // weapon+0xD0
if (mech == 0 || target == 0)
{
(void)transform;
(void)pipPosition;
(void)pipExtendedRange;
(void)pipColor;
// owningHud->AddElement( ...pip record... ); // FUN_0049b6d8(entity+0x434, ..)
return;
}
extern int BTMechTargetZone(void *mech); // mech+0x38C (mech4 TU)
int zone = BTMechTargetZone(mech);
Entity::TakeDamageMessage message(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
mech->GetEntityID(), // inflicting = owner (+0x184)
zone, // designated zone (or -1 -> cylinder)
damageData, // the 12-dword Damage @0x3A8
subsystemID); // inflictingSubsystemID (+0xD8) --
// lights up the messmgr's per-weapon
// explosion bundling (weapon+0x3E4)
SubsystemMessageManager *manager =
(SubsystemMessageManager *)mech->GetMessageManager(); // mech+0x434
if (manager != 0)
{
manager->AddDamageMessage(target, &message); // FUN_0049b6d8
}
else
{
target->Dispatch(&message); // no manager: direct (bring-up)
}
// PORT bookkeeping (score lived in the retired mech4 fire block): credit
// the delivered amount to the shooter-side score exactly as before.
extern void BTPostDamageScore(Entity *victim, Scalar damage);
extern Logical BTIsRegisteredMech(Entity *e);
if (BTIsRegisteredMech(target)
&& !((Mech *)target)->IsMechDestroyed())
{
BTPostDamageScore(target, damageData.damageAmount);
}
}