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:
co-authored by
Claude Fable 5
parent
48191d6fdf
commit
a9c3e96f20
+4
-20
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user