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
+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