From a9c3e96f20543552cafed2b8ea434a2a41b0da1a Mon Sep 17 00:00:00 2001 From: arcattack Date: Thu, 9 Jul 2026 10:13:39 -0500 Subject: [PATCH] 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 --- content/LAST.EGG | 24 +--- context/multiplayer.md | 30 ++++- game/reconstructed/btplayer.cpp | 57 ++++++++ game/reconstructed/mech.cpp | 10 ++ game/reconstructed/mech4.cpp | 231 +++++++++++++++++++------------- 5 files changed, 233 insertions(+), 119 deletions(-) diff --git a/content/LAST.EGG b/content/LAST.EGG index 1e8a83c..61255bd 100644 --- a/content/LAST.EGG +++ b/content/LAST.EGG @@ -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 - diff --git a/context/multiplayer.md b/context/multiplayer.md index a56f332..bd5d53e 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -53,10 +53,34 @@ emulator** (⚠ `NotationFile::ReadText` expects NUL-SEPARATED lines). [T2] Per-instance: `BT_LOG=` + `BT_AFFINITY=` (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 diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 4e43a80..697fcf9 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -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 diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 31ac9d6..0732c9b 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 08b29f3..1cdd704 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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. } } }