MP combat step 1 (target any peer) DONE; step 2 (cross-pod damage) localised (task #46)

STEP 1 -- target any peer mech: DONE + verified. A live-mech registry
(BTRegisterMech/BTDeregisterMech from the Mech ctor/dtor) collects
every mech -- player, dummy, and peer replicants. The boresight
world-pick walks BTGetTargetCandidates (all mechs != shooter, closest
ray hit) and the whole fire/damage block retargets from the solo
gEnemyMech to the picked hotTarget; missile/projectile validation
generalised via BTIsRegisteredMech. Verified one-box: instance A
enumerates B's mech as a live ReplicantInstance (20 zones, ownerID=3).
Solo un-regressed (pick->damage->kill clean, 28 hits + DESTROYED).

STEP 2 -- cross-pod damage: dispatch + engine reroute are CORRECT and
invoked with a valid owner, but the dispatched message doesn't reach
the master. Entity::Dispatch (ENTITY.cpp:244) reroutes a replicant's
msg via application->SendMessage(ownerID,...); BT_MP_FORCE_DMG proves A
dispatches at B's replicant with ownerID=3, yet B takes 0 STEP-6
damage. Corrects the KB's [T3] "Dispatch already reroutes": the reroute
FIRES; the break is downstream in the transport/delivery of a
dispatched entity-message (update records flow fine -- net-rx works).
Next MP task = that wire delivery. Diagnostics BT_MP_LOG /
BT_MP_FORCE_DMG retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 10:13:39 -05:00
co-authored by Claude Fable 5
parent 48191d6fdf
commit a9c3e96f20
5 changed files with 233 additions and 119 deletions
+4 -20
View File
@@ -1,8 +1,8 @@
[mission]
adventure=BattleTech
map=cavern
map=grass
scenario=freeforall
time=night
time=day
weather=clear
temperature=27
length=600
@@ -156,9 +156,8 @@ x=128
y=32
width=8
[pilots]
pilot=127.0.0.1:1502
pilot=127.0.0.1:1602
[127.0.0.1:1502]
pilot=200.0.0.96
[200.0.0.96]
hostType=0
advancedDamage=1
loadzones=1
@@ -172,20 +171,6 @@ dropzone=one
vehicle=bhk1
vehicleValue=1000
color=White
[127.0.0.1:1602]
hostType=0
advancedDamage=1
loadzones=1
name=Boreas
bitmapindex=2
experience=expert
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=bhk1
vehicleValue=1000
color=Red
[largebitmap]
bitmap=BitMap::Large::Aeolus
[BitMap::Large::Aeolus]
@@ -242,4 +227,3 @@ width=4
model=dfltrole
[Role::NoReturn]
model=noretun
+27 -3
View File
@@ -53,10 +53,34 @@ emulator** (⚠ `NotationFile::ReadText` expects NUL-SEPARATED lines). [T2]
Per-instance: `BT_LOG=<file>` + `BT_AFFINITY=<mask>` (CPU pin). Update stream ≈ 60 Hz × 144-byte
records per moving master. [T2]
## Cross-pod combat step 1+2 (task #46, 2026-07-09) [T2 findings]
**Step 1 — target ANY peer mech: DONE + verified.** A live-mech registry (`BTRegisterMech`/
`BTDeregisterMech` from the Mech ctor/dtor, btplayer.cpp) collects every Mech — player, dummy,
AND peer replicants. The mech4 boresight world-pick now walks `BTGetTargetCandidates` (all mechs
≠ shooter) and picks the CLOSEST the ray strikes, retargeting the whole fire/damage block from
the solo `gEnemyMech` to the picked `hotTarget` (missile impact + projectile validation
generalised via `BTIsRegisteredMech`). Verified one-box: instance A correctly enumerates B's
mech as a live `ReplicantInstance` (inst=4), 20 zones, `ownerID=3`, alive. Solo un-regressed
(pick→damage→kill chain clean).
**Step 2 — cross-pod damage: dispatch + reroute CORRECT, wire delivery is the GAP.** The engine
`Entity::Dispatch` (ENTITY.cpp:244) DOES reroute a replicant's message:
`if (GetInstance()==ReplicantInstance) application->SendMessage(ownerID, EntityManagerClientID,
message)`. Verified (`BT_MP_FORCE_DMG`): A dispatches TakeDamage at B's replicant with a VALID
`ownerID=3` → the reroute is invoked. BUT B's master takes **0** damage (no STEP-6 `unaimed
hit`), and the message resolves nowhere on A either. So the reroute FIRES but the dispatched
entity-message never reaches the owning master. ⚠ Corrects the earlier [T3] "Dispatch already
reroutes" to: reroute code exists + is invoked with a valid owner; the break is DOWNSTREAM in the
transport/delivery of a *dispatched* message (vs the auto-replicated update records, which flow —
`net-rx` works both ways). ⚠ `[net-tx]` trace tag is NOT wired on the send path (logs 0 even
though rx proves sends happen) — instrument the actual `Application::SendMessage``RoutePacket`
L4NET send + B-side `EntityManager` receive/deliver to localise. Diagnostics: `BT_MP_LOG`
(candidate dump), `BT_MP_FORCE_DMG` (direct replicant-dispatch proof hook).
## Remaining (P6 phase 4 / Phase 7)
Cross-pod COMBAT (target a replicant + route damage to the owning master — Entity::Dispatch already
reroutes); interactive 2-window driving; replicant GAIT animation (derive from replicated velocity);
the pod-LAN config (real IPs, bare-IP pilot entries). See [[open-questions]]. [T3]
Cross-pod damage WIRE DELIVERY (the step-2 gap above — the next MP task); interactive 2-window
driving + the `-net`-mode aim projection / drive (A's aim ray was dead — `noRay`, and
`BT_AUTODRIVE`/`BT_GOTO` didn't move A in `-net` mode); replicant GAIT animation (derive from
replicated velocity); the pod-LAN config (real IPs, bare-IP pilot entries). See [[open-questions]]. [T3]
## MP-front scout (task #45, 2026-07-09) — the P6 chain SURVIVES the combat/HUD rework [T2]
Re-ran the one-box smoke test on the current build (world-pick targeting, weapon groups, death
+57
View File
@@ -117,6 +117,63 @@
// current target. NULL when no enemy was spawned (BT_SPAWN_ENEMY unset).
Entity *gEnemyMech = 0;
//
// LIVE-MECH REGISTRY (task #46): every Mech (player, dummy, peer replicants)
// (de)registers here from its ctor/dtor. The boresight world-pick walks it to
// find target candidates -- generalising the solo gEnemyMech to MP peers. A
// plain fixed array (mech counts are tiny -- <= a few per arena) avoids any
// allocator interaction in the hot ctor path.
//
static Entity *gBTMechRegistry[32];
static int gBTMechCount = 0;
void BTRegisterMech(Entity *m)
{
if (m == 0) return;
for (int i = 0; i < gBTMechCount; ++i) // idempotent
if (gBTMechRegistry[i] == m) return;
if (gBTMechCount < 32)
gBTMechRegistry[gBTMechCount++] = m;
}
void BTDeregisterMech(Entity *m)
{
for (int i = 0; i < gBTMechCount; ++i)
{
if (gBTMechRegistry[i] == m)
{
gBTMechRegistry[i] = gBTMechRegistry[--gBTMechCount];
gBTMechRegistry[gBTMechCount] = 0;
return;
}
}
}
// The target-candidate list for a given shooter: every registered mech that is
// NOT the shooter itself. (Alive/pick filtering is the caller's -- mech4's
// world-pick already tests IsMechDestroyed + the collision-box ray.)
int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut)
{
int n = 0;
for (int i = 0; i < gBTMechCount && n < maxOut; ++i)
{
Entity *m = gBTMechRegistry[i];
if (m != 0 && m != shooter)
out[n++] = m;
}
return n;
}
// Is this entity a live registered mech? (Projectile-impact target validation
// -- replaces the solo `== gEnemyMech` guard.)
int BTIsRegisteredMech(Entity *e)
{
for (int i = 0; i < gBTMechCount; ++i)
if (gBTMechRegistry[i] == e)
return 1;
return 0;
}
//
// Reconstruction helpers / data referenced by the decomp that have no direct
// analog in the surviving WinTesla headers (CROSS-FAMILY / engine gap -- see
+10
View File
@@ -729,6 +729,13 @@ Mech::Mech(
*(Scalar *)((char *)this + 0x384) = 0.0f;
*(int *)((char *)this + 0x38c) = -1; // targeted sub-zone
// LIVE-MECH REGISTRY (task #46, MP targeting): every Mech -- player, the
// solo dummy, AND every peer replicant -- registers here so the boresight
// world-pick can test ALL of them, not just the solo gEnemyMech. The dtor
// deregisters (rare -- wrecks stay; replicants drop when a peer leaves).
extern void BTRegisterMech(Entity *m);
BTRegisterMech((Entity *)this);
//
// Install the Mech vtable and construct the embedded member objects.
//
@@ -1479,6 +1486,9 @@ Mech::~Mech()
{
Check(this);
extern void BTDeregisterMech(Entity *m); // task #46 live-mech registry
BTDeregisterMech((Entity *)this);
vtable = &PTR_FUN_0050cfa8;
// Teardown-health probe (env-gated): collisionLists@0x2e4 must be LIVE at ~Mech
+135 -96
View File
@@ -852,8 +852,11 @@ static void
if (dx*dx + dy*dy + dz*dz < (10.0f*10.0f) || p.traveled >= p.range)
{
Entity *tgt = p.target;
// Deliver only to the known-valid enemy (bring-up), same zone/damage path as the beam.
if (tgt != 0 && tgt == gEnemyMech && p.damage > 0.0f)
// Deliver to the projectile's target mech -- the launcher set p.target
// from the shooter's 0x388 slot (the picked victim; any peer mech in
// MP, task #46). A replicant target reroutes cross-pod via Dispatch.
extern int BTIsRegisteredMech(Entity *e);
if (tgt != 0 && BTIsRegisteredMech(tgt) && p.damage > 0.0f)
{
Mech *m = (Mech *)tgt;
if (m->damageZoneCount > 0)
@@ -2377,6 +2380,64 @@ void
{
extern int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3]);
extern Entity *gBTTerrainEntity; // captured by MakeEntityRenderables
extern int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut);
Entity *cand[32];
const int nc = BTGetTargetCandidates((Entity *)this, cand, 32);
// Candidate diagnostics + the cross-pod PROOF hook run INDEPENDENT of
// the aim ray (they don't need screen geometry -- and in -net mode the
// aim projection may not be live, gap-map item 5).
static float s_candLog = 0.0f; s_candLog += dt;
const int candLog = (getenv("BT_MP_LOG") && s_candLog >= 1.0f);
if (candLog) s_candLog = 0.0f;
if (candLog)
for (int ci = 0; ci < nc; ++ci)
{
Mech *m = (Mech *)cand[ci];
if (m == 0) continue;
Point3D mp = m->localOrigin.linearPosition;
DEBUG_STREAM << "[mp-cand] " << (void*)m
<< " inst=" << (int)m->GetInstance()
<< " interest=" << m->interestCount
<< " dead=" << (int)m->IsMechDestroyed()
<< " pos=(" << mp.x << "," << mp.y << "," << mp.z << ")"
<< " zones=" << m->damageZoneCount << "\n" << std::flush;
}
// CROSS-POD DAMAGE PROOF HOOK (BT_MP_FORCE_DMG, task #46): once a
// second, dispatch a real TakeDamage at the first live REPLICANT
// candidate -- isolating the Dispatch-reroute-to-owning-master path
// from the MP interactive aim/drive plumbing.
if (getenv("BT_MP_FORCE_DMG"))
{
static float s_fd = 0.0f; s_fd += dt;
if (s_fd >= 1.0f)
{
s_fd = 0.0f;
for (int ci = 0; ci < nc; ++ci)
{
Mech *m = (Mech *)cand[ci];
if (m == 0 || m->GetInstance() != ReplicantInstance
|| m->IsMechDestroyed() || m->damageZoneCount <= 0)
continue;
Damage dmg;
dmg.damageType = Damage::ExplosiveDamageType;
dmg.damageAmount = kShotDamage;
dmg.burstCount = 1;
dmg.impactPoint = m->localOrigin.linearPosition;
Entity::TakeDamageMessage td(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
GetEntityID(), -1, dmg);
((Entity *)m)->Dispatch(&td);
DEBUG_STREAM << "[mp-force] dispatched TakeDamage at replicant "
<< (void*)m << " ownerID=" << (long)m->GetOwnerID()
<< " (reroute -> master via SendMessage)\n" << std::flush;
break;
}
}
}
float rs[3], rd[3];
if (!BTGetAimRay(gBTAimX, gBTAimY, rs, rd))
{
@@ -2386,11 +2447,31 @@ void
{
Point3D rayStart(rs[0], rs[1], rs[2]);
Vector3D rayDir(rd[0], rd[1], rd[2]);
if (gEnemyMech != 0 && !((Mech *)gEnemyMech)->IsMechDestroyed()
&& ((Mech *)gEnemyMech)->PickRayHit(rayStart, rayDir, 4000.0f, &hotPoint))
// MP TARGETING (task #46): walk EVERY other living mech (the solo
// dummy AND every peer replicant, from the live-mech registry) and
// pick the CLOSEST one the boresight ray strikes -- generalising
// the solo gEnemyMech.
float bestDist = 1e30f;
for (int ci = 0; ci < nc; ++ci)
{
hotTarget = gEnemyMech;
pickTarget = gEnemyMech;
Mech *m = (Mech *)cand[ci];
if (m == 0 || m->IsMechDestroyed())
continue;
Point3D hp;
if (!m->PickRayHit(rayStart, rayDir, 4000.0f, &hp))
continue;
float dx = hp.x - rs[0], dy = hp.y - rs[1], dz = hp.z - rs[2];
float d = dx*dx + dy*dy + dz*dz;
if (d < bestDist)
{
bestDist = d;
hotTarget = cand[ci];
hotPoint = hp;
}
}
if (hotTarget != 0)
{
pickTarget = hotTarget;
pickPoint = hotPoint;
++gAimHits;
}
@@ -2540,27 +2621,25 @@ void
gBTMissileTrigger = (missileWanted && targetInArc) ? (gBTMissileTrigger ? 0 : 1) : 0;
}
if (gEnemyMech != 0)
{
Point3D enemyPos = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
// The VICTIM under the boresight (task #46: any peer mech, not just
// the solo gEnemyMech). Range/log/fire all key off THIS mech.
Entity *victim = hotTarget;
Point3D victimPos = (victim != 0)
? ((Mech *)victim)->localOrigin.linearPosition
: localOrigin.linearPosition;
// damage routes ONLY when the boresight pick is the enemy mech
const int designated = (hotTarget != 0);
float ddx = enemyPos.x - localOrigin.linearPosition.x;
float ddy = enemyPos.y - localOrigin.linearPosition.y;
float ddz = enemyPos.z - localOrigin.linearPosition.z;
float ddx = victimPos.x - localOrigin.linearPosition.x;
float ddy = victimPos.y - localOrigin.linearPosition.y;
float ddz = victimPos.z - localOrigin.linearPosition.z;
float range = (float)Sqrt((double)(ddx*ddx + ddy*ddy + ddz*ddz));
// THE AUTHENTIC RANGE GATE (FireWeapon @004bace8 :7758 [T1]): damage
// applies when dist <= the weapon's EFFECTIVE range = (1 - host-zone
// damage) x its AUTHORED WeaponRange (BLH: lasers 500, missiles 800,
// PPCs 900 m -- the [hud] pip dump) -- the
// per-weapon targetWithinRange flag UpdateTargeting computes each
// frame. Any live weapon within reach lands the aggregate shot.
// (Replaces the old kWeaponRange=100 bring-up constant, which
// silently blanked damage between 100 and the real 500 -- lock at
// spawn range 120 dealt nothing until you closed in; user-reported.)
// PPCs 900 m -- the [hud] pip dump) -- the per-weapon targetWithinRange
// flag UpdateTargeting computes each frame. Any live weapon in reach
// lands the aggregate shot.
int anyWeaponInRange = 0;
for (int wi = 0; wi < GetSubsystemCount(); ++wi)
{
@@ -2579,29 +2658,21 @@ void
{
gTargetLogAccum = 0.0f;
DEBUG_STREAM << "[target] aim=(" << gBTAimX << "," << gBTAimY << ")"
<< (hotTarget != 0 ? " ENEMY under boresight (aimed)"
<< (victim != 0 ? " MECH under boresight (aimed)"
: (pickTarget != 0 ? " terrain downrange (beam at scenery)"
: " sky (no target, no discharge)"))
<< " range=" << range
<< (anyWeaponInRange ? " IN WEAPON RANGE" : "")
<< " [enemyPicks=" << gAimHits << " groundPicks=" << gAimGround
<< " [mechPicks=" << gAimHits << " groundPicks=" << gAimGround
<< " noRay=" << gAimNoRay << "]"
<< "\n" << std::flush;
gAimHits = 0; gAimNoRay = 0; gAimGround = 0;
}
// --- FIRING (bring-up): on the trigger, with a target in range and the
// weapon off cooldown, spawn an Explosion at the target. This is the
// real engine fire->effect->render chain (Explosion::Make + the
// engine's explosion renderable) standing in for the per-weapon beam
// (Emitter::FireWeapon) until the emitter subsystem + its beam
// renderable are reconstructed. The shot is gated exactly like the
// real weapon: HasActiveTarget (we just set mech+0x388) AND in range.
if (gFireCooldown > 0.0f)
gFireCooldown -= dt;
const int fireWanted = gBTDrive.fireForced || gBTDrive.fire;
// (the fire-channel pulses moved ABOVE the enemy block -- task #43a)
// Resolve the "explode" effect resource once.
if (gExplodeReady == 0)
@@ -2626,31 +2697,24 @@ void
}
}
if (fireWanted && designated && targetInArc && gFireCooldown <= 0.0f
&& anyWeaponInRange && gExplodeReady == 1) // authentic gate (computed above)
// FIRE + DAMAGE: only vs a MECH under the boresight (terrain/sky picks
// draw beams but deal no damage). Dispatch routes to the picked victim
// -- for a REPLICANT this is a cross-pod hit: Entity::Dispatch reroutes
// the TakeDamageMessage to the owning master over the wire (task #46).
if (fireWanted && victim != 0 && targetInArc && gFireCooldown <= 0.0f
&& anyWeaponInRange && gExplodeReady == 1)
{
gFireCooldown = kFireCooldown;
++gShotCount;
// Impact point (tasks #36/#41): damage happens ONLY when the enemy
// is under the boresight (designated == hotTarget != 0, checked in
// the gate above), so the shot always lands at the PICKED HULL
// POINT -- the STEP-6 cylinder lookup resolves damage to the zone
// under the boresight (aimed fire: leg shots hit legs, torso shots
// the torso). Terrain picks never reach here (beam + scenery
// impact only, no damage dispatch).
// Impact = the PICKED HULL POINT -> the STEP-6 cylinder lookup
// resolves the zone under the boresight (aimed fire).
Point3D impact = hotPoint;
Origin exp_origin = ((Mech *)gEnemyMech)->localOrigin;
Origin exp_origin = ((Mech *)victim)->localOrigin;
exp_origin.linearPosition = impact; // at the hit point
// IMPACT FRAME: the .PFX hit effects are authored with local -Z =
// "out of the struck armor" (DAFC offsets/velocities spray -Z).
// That is the IMPACT normal -- toward the SHOOTER -- not the
// victim's body front: with the victim's own quat a rear/side hit
// flashed on the FAR (front) side of the mech. Build the frame as
// a yaw with -Z aimed from the victim at the shooter. Engine yaw
// convention (MATRIX.cpp:196-209): forward -Z = (-sin y, 0, -cos y)
// -> yaw = atan2(ddx, ddz) points -Z at the shooter. [T0]
// IMPACT FRAME: -Z aimed from the victim at the shooter (the .PFX
// hit spray convention; yaw = atan2(ddx, ddz)). [T0]
exp_origin.angularPosition =
EulerAngles(0.0f, (Scalar)atan2((double)ddx, (double)ddz), 0.0f);
@@ -2662,7 +2726,7 @@ void
gExplodeRes,
Explosion::DefaultFlags,
exp_origin,
gEnemyMech->GetEntityID(), // entity hit
victim->GetEntityID(), // entity hit
GetEntityID()); // shooter (this mech)
Explosion *shot = Explosion::Make(&exp_message);
@@ -2672,34 +2736,20 @@ void
DEBUG_STREAM << "[fire] SHOT #" << gShotCount
<< " -> explosion at target (range=" << range << ")\n" << std::flush;
}
else
{
DEBUG_STREAM << "[fire] Explosion::Make returned NULL\n" << std::flush;
}
// --- DAMAGE (real, STEP 6): dispatch UNAIMED (zone == -1) with the
// beam's world entry point; Mech::TakeDamageMessageHandler resolves
// the zone from the cylinder hit-location table (the STEP-6
// reconstruction) and the base handler routes it to
// Mech__DamageZone::TakeDamage (the real armor/structure model).
// Hits therefore land on the EXTERIOR zone facing the shooter
// (arm/leg/torso -- zones with visible segments that wreck), and
// internal vitals die only through the authentic destruction
// cascade (RecurseSegmentTable / SendSubsystemDamage) -- no more
// invisible one-shot kills on the soft internal vital zone.
if (gEnemyMech->damageZoneCount > 0)
// beam's world entry point. Mech::TakeDamageMessageHandler
// resolves the zone from the cylinder table; the base handler
// 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)
{
// (impact computed above -- shared with the hit-explosion origin)
Damage dmg; // default-constructed
// Explosive: the weapon effect is an Explosion (explosive), not an
// energy beam. Also the correct type to exercise the zone armour/
// structure/death model now -- EnergyDamageType(4) shorts attached
// generators via criticalSubsystems[]->plug, which needs the REAL
// subsystem roster (still RECON_SUBSYS stubs -> plug resolves null).
Damage dmg;
dmg.damageType = Damage::ExplosiveDamageType;
dmg.damageAmount = kShotDamage;
dmg.burstCount = 1;
dmg.impactPoint = impact; // world impact point
dmg.impactPoint = impact;
Entity::TakeDamageMessage take_damage(
Entity::TakeDamageMessageID,
@@ -2707,37 +2757,26 @@ void
GetEntityID(), // inflicting = this (shooter)
-1, // UNAIMED -> receiver's cylinder resolves
dmg);
gEnemyMech->Dispatch(&take_damage);
victim->Dispatch(&take_damage);
// gauge scoring wave (Step 6): credit the local player for damage
// dealt -> SCORE climbs per hit (currentScore += tonnageRatio*award).
// No score for pounding the wreck (damage still applies -- zones
// clamp at 1.0, further segments wreck visibly).
if (!gEnemyDestroyed)
BTPostDamageScore(gEnemyMech, kShotDamage);
// SCORE the damage (skip once the victim is dead).
if (!((Mech *)victim)->IsMechDestroyed())
BTPostDamageScore(victim, kShotDamage);
// The handler wrote the resolved zone back into the message.
int zone = take_damage.damageZone;
if (zone >= 0 && zone < gEnemyMech->damageZoneCount)
if (zone >= 0 && zone < ((Mech *)victim)->damageZoneCount)
{
Scalar s = ((Mech *)gEnemyMech)->Zone(zone)->damageLevel; // [0,1], 1.0=destroyed
DEBUG_STREAM << "[damage] hit zone " << zone << "/"
<< gEnemyMech->damageZoneCount
<< " structure=" << s << "\n" << std::flush;
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;
}
}
// DEATH detection + effects moved to the VICTIM's own death
// transition (UpdateDeathState, task #42): the killing blow can be
// a MISSILE landing frames after this block (or collision damage),
// and post-#41 the boresight pick skips a dead mech so this block
// never ran against it again -- the death chain silently skipped
// (internally dead, smoking, standing, invulnerable). The
// transition fires once for ANY kill source. (The
// BT_ENABLE_TEARDOWN cdb repro harness was removed with this
// block -- its findings are recorded in docs/HARD_PROBLEMS.md;
// re-add at the transition if the P5 work resumes. The wreck
// STAYS -- teardown = the P5 base-region crash.)
// DEATH effects fire at the VICTIM's own death transition
// (UpdateDeathState, task #42) -- MP-correct: the master runs it.
}
}
}