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:
co-authored by
Claude Fable 5
parent
77190c93e5
commit
fd055281a8
@@ -630,12 +630,20 @@ BTL4Application::SharedData
|
||||
if (resource_description != NULL)
|
||||
{
|
||||
resource_description->Lock(); // FUN_00406cd0
|
||||
DEBUG_STREAM << "[ctrlmap] installing '" << primary_mapping_name
|
||||
<< "' table: " << *(int *)resource_description->resourceAddress
|
||||
<< " mappings on entity " << (void *)viewing_entity << std::endl;
|
||||
controls->CreateStreamedMappings( // FUN_0047703c
|
||||
viewing_entity,
|
||||
(int*)resource_description->resourceAddress
|
||||
);
|
||||
resource_description->Unlock();
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "[ctrlmap] NO '" << primary_mapping_name
|
||||
<< "' sub-table for this mech" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -143,7 +143,10 @@ static const Scalar CriticalDamageMax = 1.0f; // _DAT_0049ce4c
|
||||
// Probability of re-using the previous child zone inside the reuse window.
|
||||
// (Ghidra read the operand at @0049c514; exact value not cleanly recoverable
|
||||
// from the pseudo-C -- it behaves as a high "stay on the same spot" bias.)
|
||||
static const Scalar LODReuseHysteresis = 0.82f; // _DAT_0049c514 (TODO: verify)
|
||||
static const Scalar LODReuseHysteresis = 0.33f; // _DAT_0049c514 -- a DOUBLE in the
|
||||
// binary (bytes 1f85eb51b81ed53f =
|
||||
// 0.33); the old 0.82 was a misread
|
||||
// (task #8 audit)
|
||||
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -285,7 +285,9 @@ class CockpitHud;
|
||||
// @004b9728 -- register the targeting "pip" / fire marker with the
|
||||
// owning Mech's cockpit HUD manager.
|
||||
void
|
||||
DrawWeaponPip(const LinearMatrix &transform);
|
||||
SendDamageMessage(Entity *target); // @004b9728 (ex "DrawWeaponPip" -- the
|
||||
// weapon-fire damage submission into the
|
||||
// owner's SubsystemMessageManager, task #8)
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Construction and Destruction
|
||||
|
||||
Reference in New Issue
Block a user