Combat: FIX weapon effectiveRange -- it degraded with heatLoad, not host-zone damage (task #50)
MechWeapon::UpdateTargeting computed effectiveRange = (1 - heatLoad) * weaponRange, reading the weapon's own inherited HeatableSubsystem heatLoad. The authentic decomp (@004b9bdc:6983) reads *(weapon+0xE0)+0x158 = Subsystem::damageZone->damageLevel -- i.e. effectiveRange = (1 - HOST-ZONE DAMAGE) * weaponRange. Same @0xE0-DamageZone-vs- heat misattribution already corrected in HeatSink::UpdateCoolant (heat.cpp:803). Impact: for a charge/discharge weapon (ER laser) the weapon's OWN heatLoad swings 0..1 every fire cycle, so effectiveRange collapsed toward 0 and the weapon was perpetually "out of range" -> Emitter::FireWeapon's `if (dist <= effectiveRange)` gate skipped SendDamageMessage -> NO damage submission and hence NO impact explosion. The beam still rendered (beamFlag/beamEndpoint set before the gate), so the shot LOOKED like a hit but did nothing -- the user-reported "lackluster/absent laser hits, esp. the ER medium, on mechs AND buildings". PPCs mostly worked only because their heatLoad happened to sit low/stable. Fix: read the QUALIFIED this->Subsystem::damageZone->damageLevel (the MechSubsystem shadow is a shim -- heat.cpp:812) so an UNDAMAGED weapon holds its full, STABLE weaponRange, and range shortens only as the weapon's host zone takes battle damage. Verified (parked in range of a building, autofire): laser effRange 500 STABLE (was fluctuating 0/59/340/424 -> mostly out of range); impact explosions 13 in 22s (11 laser id=16 + 2 PPC), up from ~2. Lasers now consistently damage + spawn FX. Also adds env-gated diagnostics used to root-cause this: [fireW] range trace + per-weapon explID (emitter.cpp), and BT_FIRE_AT_STRUCT (mech4.cpp) which designates the nearest world structure so weapon-vs-structure fire can be tested without the screen aim ray. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
de8f6d02c1
commit
aab7a8a137
@@ -61,6 +61,16 @@ Fixed-torso mechs (the BLH: `TorsoHorizontalEnabled=0` — no jointtorso in the
|
|||||||
boresight dead-ahead. Once locked, `Emitter::FireWeapon` converges with NO aim/arc test
|
boresight dead-ahead. Once locked, `Emitter::FireWeapon` converges with NO aim/arc test
|
||||||
(part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange =
|
(part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange =
|
||||||
dist < (1 − hostZoneDamage) × weaponRange`. The 0x388 WRITER is in the same un-exported gap.
|
dist < (1 − hostZoneDamage) × weaponRange`. The 0x388 WRITER is in the same un-exported gap.
|
||||||
|
⚠ **`hostZoneDamage` is `Subsystem::damageZone->damageLevel` (weapon `@0xE0 → +0x158`), NOT
|
||||||
|
heatLoad** — the port originally computed `effectiveRange = (1 − heatLoad) × weaponRange`
|
||||||
|
(mechweap.cpp), the SAME `@0xE0`-DamageZone-vs-heat misattribution corrected in
|
||||||
|
`HeatSink::UpdateCoolant`. For a charge/discharge weapon (ER laser) the weapon's own `heatLoad`
|
||||||
|
swings 0→1 every cycle, so `effectiveRange` collapsed to ~0 and the weapon was perpetually "out
|
||||||
|
of range" → `SendDamageMessage` never ran → NO damage and NO impact explosion (the "lackluster/
|
||||||
|
absent laser hits, esp. the ER medium, on mechs AND buildings" report). FIXED 2026-07-15: read
|
||||||
|
the QUALIFIED `this->Subsystem::damageZone->damageLevel`; an undamaged weapon now holds full
|
||||||
|
STABLE `weaponRange`. Verified: laser `effRange` 500 stable (was 0/59/340/424), impact explosions
|
||||||
|
13/22s (was ~2). [T2] See [[reconstruction-gotchas]] (`@0xE0` shadow/heat class).
|
||||||
**Port status: RECONSTRUCTED + runtime-verified (tasks #36/#39/#58) [T2].** The chain: the
|
**Port status: RECONSTRUCTED + runtime-verified (tasks #36/#39/#58) [T2].** The chain: the
|
||||||
cockpit eye inherits the twist AUTHENTICALLY through the draw traversal (`Torso::PushTwist` →
|
cockpit eye inherits the twist AUTHENTICALLY through the draw traversal (`Torso::PushTwist` →
|
||||||
hinge `rotationAmount` → `HingeRenderable::Execute` multiplies the live hinge into the matrix
|
hinge `rotationAmount` → `HingeRenderable::Execute` multiplies the live hinge into the matrix
|
||||||
|
|||||||
@@ -271,6 +271,15 @@ void
|
|||||||
Scalar dist = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // FUN_004dd138
|
Scalar dist = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); // FUN_004dd138
|
||||||
beamScale.z = dist / graphicLength; // 0x434 (== beamScale[2]) = dist / 0x438
|
beamScale.z = dist / graphicLength; // 0x434 (== beamScale[2]) = dist / 0x438
|
||||||
|
|
||||||
|
if (getenv("BT_FIRE_LOG"))
|
||||||
|
{
|
||||||
|
Entity *st = (owner != 0) ? *(Entity **)((char *)owner + 0x388) : 0;
|
||||||
|
DEBUG_STREAM << "[fireW] " << GetName() << " dist=" << (float)dist
|
||||||
|
<< " effRange=" << (float)effectiveRange
|
||||||
|
<< " inRange=" << (int)(dist <= effectiveRange)
|
||||||
|
<< " explID=" << (long)GetExplosionResourceID()
|
||||||
|
<< " tgt=" << (void *)st << "\n" << std::flush;
|
||||||
|
}
|
||||||
if (dist <= effectiveRange) // this+0x328
|
if (dist <= effectiveRange) // this+0x328
|
||||||
{
|
{
|
||||||
// THE AUTHENTIC DAMAGE SUBMISSION (task #8; binary @004bace8:7758-7764):
|
// THE AUTHENTIC DAMAGE SUBMISSION (task #8; binary @004bace8:7758-7764):
|
||||||
@@ -349,6 +358,7 @@ void
|
|||||||
<< " pct=" << (float)rechargeLevel
|
<< " pct=" << (float)rechargeLevel
|
||||||
<< " tgt=" << (void *)((owner != 0)
|
<< " tgt=" << (void *)((owner != 0)
|
||||||
? *(Entity **)((char *)owner + 0x388) : 0)
|
? *(Entity **)((char *)owner + 0x388) : 0)
|
||||||
|
<< " explID=" << (long)GetExplosionResourceID()
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
|
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
|
||||||
|
|||||||
@@ -4502,6 +4502,43 @@ void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DETERMINISTIC FIRE-AT-STRUCTURE TEST (BT_FIRE_AT_STRUCT, task #50 verify):
|
||||||
|
// the headless aim ray (BTGetAimRay) is flaky without a focused window, so
|
||||||
|
// this bypasses it -- designate the NEAREST world structure (360 sweep) as
|
||||||
|
// the target every frame so BT_AUTOFIRE can be tested against a real, stable,
|
||||||
|
// in-range building. Diag only, off by default.
|
||||||
|
if (getenv("BT_FIRE_AT_STRUCT"))
|
||||||
|
{
|
||||||
|
extern Entity *gBTTerrainEntity;
|
||||||
|
Point3D eye = localOrigin.linearPosition; eye.y += 30.0f;
|
||||||
|
Point3D best; float bestR = 1e30f; bool got = false;
|
||||||
|
for (int a = 0; a < 24; ++a)
|
||||||
|
{
|
||||||
|
double th = (double)a * (6.2831853071795862 / 24.0);
|
||||||
|
Vector3D d((float)cos(th), 0.0f, (float)sin(th));
|
||||||
|
Point3D hp;
|
||||||
|
if (WorldStructurePick(eye, d, 2000.0f, &hp))
|
||||||
|
{
|
||||||
|
float dx = hp.x-eye.x, dz = hp.z-eye.z;
|
||||||
|
float r = (float)Sqrt(dx*dx + dz*dz);
|
||||||
|
if (r < bestR) { bestR = r; best = hp; got = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (got && gBTTerrainEntity != 0)
|
||||||
|
{
|
||||||
|
MECH_TARGET_ENTITY(this) = gBTTerrainEntity;
|
||||||
|
MECH_TARGET_SUBIDX(this) = -1;
|
||||||
|
MECH_TARGET_POS(this) = best;
|
||||||
|
static float s_fas = 0.0f; s_fas += dt;
|
||||||
|
if (getenv("BT_FIRE_LOG") && s_fas >= 1.0f)
|
||||||
|
{
|
||||||
|
s_fas = 0.0f;
|
||||||
|
DEBUG_STREAM << "[fireat] nearest struct (" << best.x << ","
|
||||||
|
<< best.y << "," << best.z << ") r=" << bestR << "\n" << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HUD feeds: the range caret + the hotbox (world point + state) + the
|
// HUD feeds: the range caret + the hotbox (world point + state) + the
|
||||||
// recovered-Execute instruments (compass, twist tape, group mask).
|
// recovered-Execute instruments (compass, twist tape, group mask).
|
||||||
// The range caret tracks the PICK (authentic: :5639 computes it from
|
// The range caret tracks the PICK (authentic: :5639 computes it from
|
||||||
|
|||||||
@@ -500,8 +500,21 @@ Logical
|
|||||||
rangeToTarget = (Scalar)Sqrt( // FUN_004dd138
|
rangeToTarget = (Scalar)Sqrt( // FUN_004dd138
|
||||||
delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
|
delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
|
||||||
|
|
||||||
// 1.0f == _DAT_004b9c98; heatLoad is the inherited HeatableSubsystem load.
|
// AUTHENTIC (decomp @004b9bdc:6983): effectiveRange = (1 - hostZoneDamage) *
|
||||||
effectiveRange = (1.0f - heatLoad) * weaponRange; // *0x32c
|
// weaponRange, where hostZoneDamage is this weapon's OWN DamageZone.damageLevel
|
||||||
|
// (the QUALIFIED Subsystem::damageZone @0xE0 -> damageLevel +0x158), NOT
|
||||||
|
// heatLoad. The old `heatLoad` read was the SAME @0xE0-vs-heat misattribution
|
||||||
|
// already corrected in HeatSink::UpdateCoolant (heat.cpp:803): for a charge/
|
||||||
|
// discharge weapon (ER laser) the weapon's OWN heatLoad swings 0..1 every cycle,
|
||||||
|
// so effectiveRange collapsed toward 0 and the weapon was perpetually "out of
|
||||||
|
// range" -- no damage submission, hence no impact explosion (user report:
|
||||||
|
// lackluster/absent laser hits on mechs AND buildings, esp. the ER medium).
|
||||||
|
// An UNDAMAGED weapon has damageLevel 0 -> full, STABLE weaponRange. Use the
|
||||||
|
// QUALIFIED engine zone -- MechSubsystem::damageZone is a shim SHADOW (heat.cpp:812).
|
||||||
|
// 1.0f == _DAT_004b9c98.
|
||||||
|
::DamageZone *hostZone = this->Subsystem::damageZone; // @0xE0
|
||||||
|
Scalar hostZoneDamage = (hostZone != 0) ? hostZone->damageLevel : 0.0f; // +0x158
|
||||||
|
effectiveRange = (1.0f - hostZoneDamage) * weaponRange; // *0x32c
|
||||||
|
|
||||||
targetWithinRange = (effectiveRange > rangeToTarget) ? True : False;
|
targetWithinRange = (effectiveRange > rangeToTarget) ? True : False;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user