#171 IMPLEMENTED: the AC is trigger-time hitscan again (@004bc104 restored: counter/TracerInterval batching, ComputeTimeOfFlight inside the tracer branch refreshing LIVE effectiveRange, dist<=+0x328 fire-time gate, ONE single-panel batched damage to the locked target via @004b9728; the flying round is the cosmetic 0xBCD tracer, damage 0 -- shell class 0xBD1 stays dead). Missiles keep flight, corrected to the binary: lock-drop = movementMode 2||9 exactly (@0049fb54, tracks THROUGH the fall), proximity fuze 4.0 (_DAT_004bf5a4 -- was misused as a turn gain), salvo roll floor-of-1 REMOVED (binary sender rolls 0; the receiver burst do-while is the floor, mech.cpp:1229). Gauss untouched pending the era witnesses (shipped binary deals NO Gauss damage -- group question on #171). Verification: line-by-line vs part_013.c:16023-16125/18260-18285 (verify workflow + review); end-to-end receipts from the engaged bench run: type=1 burst=1 hitscan hits incl. a 13-round batch, all DETs dmg=0. Local rig cannot form avatar locks for a clean re-run (rig, not feature) -- Friday's field receipts are armed and decisive. Closes the #168 mis-generalization; #112 single-panel restored
This commit is contained in:
@@ -1746,11 +1746,29 @@ static void
|
||||
// autocannon could not miss a moving target. Track the entity for the
|
||||
// contact test; only GUIDED rounds also steer onto it.
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (p.target != 0 && BTIsRegisteredMech(p.target)
|
||||
&& !((Mech *)p.target)->IsMechDestroyed())
|
||||
if (p.target != 0 && BTIsRegisteredMech(p.target))
|
||||
{
|
||||
p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition;
|
||||
p.targetPos.y += p.aimOffsetY;
|
||||
// #171 LOCK DROP predicate = @0049fb54 EXACTLY: movementMode 2||9,
|
||||
// the latched death modes -- NOT IsMechDestroyed(), whose extra
|
||||
// graphicAlarm>=9 term fires at the vital-kill TRIGGER, seconds
|
||||
// before the collapse (modes 5-8) reaches 2/9. The binary seeker
|
||||
// keeps TRACKING (and its 4.0u fuze stays live) all through the
|
||||
// fall; it releases only once the wreck latches disabled
|
||||
// (@004bef78: IsKindOf(Mech) && FUN_0049fb54 -> seeker+0xfc = 0).
|
||||
const int mm = ((Mech *)p.target)->MovementMode();
|
||||
if (!(mm == 2 || mm == 9))
|
||||
{
|
||||
p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition;
|
||||
p.targetPos.y += p.aimOffsetY;
|
||||
}
|
||||
else if (p.guided)
|
||||
{
|
||||
// Seeker released: the round flies on UNGUIDED; only geometry
|
||||
// can detonate it now. (The old port kept steering to the
|
||||
// corpse's last position and could still sphere-contact it.)
|
||||
p.guided = 0;
|
||||
p.target = 0; // fuze disarmed with the lock
|
||||
}
|
||||
}
|
||||
if (p.guided)
|
||||
{
|
||||
@@ -1910,7 +1928,15 @@ static void
|
||||
cz = p.targetPos.z - hitPos.z;
|
||||
contactD2 = cx*cx + cy*cy + cz*cz;
|
||||
}
|
||||
const int contact = (contactD2 < (10.0f*10.0f));
|
||||
// #171 FUZE RADIUS: a GUIDED damage round detonates at the binary's
|
||||
// proximity fuze _DAT_004bf5a4 = 4.0 (seeker rangeToTarget +0x10C <
|
||||
// 4.0, @004bef78 -- the SAME constant the old steering comment
|
||||
// misattributed as "MissileTurnGain"; the 4.0/8.0 turn rates above are
|
||||
// PORT TUNING [T3], not that constant). Cosmetic rounds (damage 0:
|
||||
// AC tracers, cluster visuals) keep the wider 10u so their burst
|
||||
// visuals still read at speed.
|
||||
const Scalar fuzeR = (p.guided && p.damage > 0.0f) ? 4.0f : 10.0f;
|
||||
const int contact = (contactD2 < fuzeR * fuzeR);
|
||||
if (!contact && (p.age >= p.ttl || (p.guided && p.pos.y < -1.0f)))
|
||||
{
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
@@ -1978,9 +2004,22 @@ static void
|
||||
int bursts = (p.splashBurst > 0) ? p.splashBurst : 1;
|
||||
if (bursts > 1)
|
||||
{
|
||||
// #171: the binary roll (part_013.c:18272-18281) has NO
|
||||
// floor-of-1 -- rolled = min(Random(n) + n/4, n), and
|
||||
// Random(n) is 0..n-1, so n<4 can roll ZERO. The
|
||||
// VICTIM side is what floors it: the binary burst loop
|
||||
// is a DO-WHILE (part_012.c:14660-14685 -- local_2c =
|
||||
// burstCount, apply, decrement, `if (local_2c < 1)
|
||||
// break` AFTER the body), so a 0-burst message still
|
||||
// applies ONE burst; mech.cpp:1229's `burstsLeft < 1
|
||||
// -> 1` guard is the faithful mirror of that shape.
|
||||
// The old sender-side floor made the end-to-end count
|
||||
// identical, but shipped a message field the binary
|
||||
// never sends (burstCount 1 where the binary says 0);
|
||||
// roll it exactly as the binary does and let the
|
||||
// receiver's do-while supply the floor.
|
||||
int rolled = Random(bursts) + (bursts >> 2);
|
||||
if (rolled > bursts) rolled = bursts;
|
||||
if (rolled < 1) rolled = 1;
|
||||
bursts = rolled;
|
||||
}
|
||||
dmg.burstCount = bursts;
|
||||
|
||||
@@ -1002,10 +1002,9 @@ void
|
||||
{
|
||||
Check(this);
|
||||
|
||||
// Refresh the lead solution (leadPosition / targeting) for the spawn below
|
||||
// (@004bc06c; the recovered sim no longer calls it -- best-effort placement
|
||||
// inside the unrecovered @4bc104 body, which is where the lead data is used).
|
||||
ComputeTimeOfFlight(); // @004bc06c
|
||||
// (ComputeTimeOfFlight @004bc06c is called INSIDE the tracer branch below,
|
||||
// exactly where @004bc104 calls it -- :16091, between the muzzle resolve and
|
||||
// the range gate. The old top-of-body placement predated the re-export.)
|
||||
|
||||
// THE FIRING HEAT (task #9; was "a separate pre-existing gap"): the
|
||||
// binary adds heatCostToFire RAW to the weapon's own pendingHeat --
|
||||
@@ -1047,27 +1046,79 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
// WAVE 7 Phase B: launch one flying ballistic round toward the owner's target (port
|
||||
// reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine
|
||||
// Entity base mismatch -- see BTPushProjectile / mech4.cpp). Speed = |launchVelocity|.
|
||||
//
|
||||
// #171 (2026-08-13) -- THE AUTHENTIC AC: TRIGGER-TIME HITSCAN (@004bc104
|
||||
// [T1], the body the stale banner hid; the prior flying-damage round was
|
||||
// the #168 mis-generalization from the MISSILE launcher). Binary shape:
|
||||
// increment the round counter (+0x408); every TracerInterval-th (+0x438)
|
||||
// round, dist = |owner aimPoint(+0x37c) - muzzle| gated against the LIVE
|
||||
// effectiveRange (+0x328, the same gate the Emitter uses) -> ONE
|
||||
// single-panel Damage (amount = batchedCount x DamageAmount) to the
|
||||
// LOCKED target *(mech+0x388) via SendDamageMessage @004b9728, AT FIRE
|
||||
// TIME. Out of range: the batch silently discards. A locked AC inside
|
||||
// range cannot miss; nothing about the hit rides the visible round.
|
||||
// (This also restores the binary's ONE-message-per-batch = single-panel
|
||||
// damage -- closing #112's multi-zone shotgun, which was the port's
|
||||
// per-round zone re-roll scatter.)
|
||||
//
|
||||
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
|
||||
// The owner's target slots (raw 0x388/0x37c -- the same raw offsets mech4's
|
||||
// targeting block writes; a consistent producer/consumer pair on our
|
||||
// compiled object). Task #41: the slots hold the WORLD PICK -- the enemy
|
||||
// mech under the boresight, or the terrain downrange (a missile fired at
|
||||
// the scenery flies to the ground point and detonates without damage), or
|
||||
// null at the sky (BTPushProjectile refuses; the weapon's own 0x388 gate
|
||||
// wouldn't have fired either).
|
||||
void *target = (o != 0) ? *(void **)(o + 0x388) : 0;
|
||||
Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle;
|
||||
|
||||
++tracerCounter; // @0x408 -- incremented BEFORE the test
|
||||
// @4bc104:16079: bVar1 = (interval != 0) && (counter % interval == 0).
|
||||
// interval==0 -> bVar1 never set: NO damage ever, counter never resets
|
||||
// (and the short-circuit means no divide) -- the port matches exactly.
|
||||
if (tracerInterval != 0 && (tracerCounter % tracerInterval) == 0)
|
||||
{
|
||||
// @4bc104:16088-16091: muzzle transform (@004b9948, resolved above as
|
||||
// `muzzle`), then the lead/targeting refresh -- INSIDE the tracer
|
||||
// branch, not per-round. (Also re-derives effectiveRange via
|
||||
// UpdateTargeting, so the gate below reads the LIVE value.)
|
||||
ComputeTimeOfFlight(); // @004bc06c
|
||||
Vector3D toAim;
|
||||
toAim.Subtract(targetPos, muzzle); // :16096 FUN_00408644(aimPoint(+0x37c), muzzle)
|
||||
Scalar dist = (Scalar)sqrtf(toAim.x*toAim.x + toAim.y*toAim.y + toAim.z*toAim.z);
|
||||
if (dist <= effectiveRange && target != 0) // :16098 gate (+0x328); the null test is the
|
||||
{ // port's guard (binary trusts 0x388!=0 upstream)
|
||||
// @4bc104 fills EXACTLY TWO fields of the batched record (:16100-16120):
|
||||
// damageAmount = counter x authored amount (:16110)
|
||||
// damageForce = rootSegRot . leadPosition(+0x420) x float(+0x41c)
|
||||
// impactPoint is NOT set here -- SendDamageMessage @004b9728 sets it
|
||||
// (FUN_00408440(dmg+0x20, mech+0x37c) = the aim point); the port's
|
||||
// SendDamageMessage doesn't do that write, so it is FOLDED here as
|
||||
// impactPoint = targetPos (net-identical). burstCount RIDES the
|
||||
// authored record untouched (Damage ctor = 1 for an AC; only
|
||||
// MissileLauncher re-authors it, and that class overrides FireWeapon).
|
||||
// damageForce divergence [T3, deliberate]: the binary's value is the
|
||||
// lead point rotated by the mech root-segment matrix scaled by the
|
||||
// float at +0x41c (a dword lifted from the tracer-model record --
|
||||
// direction-equivalent junk; gyro rattle consumes DIRECTION only,
|
||||
// and the Emitter's own path authors force = target - muzzle
|
||||
// (emitter.cpp:360 @0x3B0)), so the port uses the same toAim shape.
|
||||
Scalar batched = (Scalar)tracerCounter * damageData.damageAmount; // :16110
|
||||
Damage batchSave = damageData; // binary builds a LOCAL copy (:16100);
|
||||
damageData.damageAmount = batched; // the port mutates+restores because our
|
||||
damageData.impactPoint = targetPos; // SendDamageMessage reads the member
|
||||
damageData.damageForce = toAim;
|
||||
SendDamageMessage((Entity *)target); // @004b9728 -- at fire time
|
||||
damageData = batchSave;
|
||||
}
|
||||
tracerCounter = 0; // :16123 -- resets on HIT and MISS alike,
|
||||
} // but ONLY on tracer rounds
|
||||
|
||||
// The VISIBLE round: the binary's 0xBCD renderer tracer, cosmetic only
|
||||
// (the shell entity 0xBD1 is dead code -- nothing shipped ever spawns a
|
||||
// damage-carrying ballistic round). Fly the pool round as that visual:
|
||||
// damage 0, straight at the pick, authentic muzzle speed.
|
||||
Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
||||
+ launchVelocity.y*launchVelocity.y
|
||||
+ launchVelocity.z*launchVelocity.z);
|
||||
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount,
|
||||
BTPushProjectile(muzzle, o, target, targetPos, speed, 0.0f /*cosmetic tracer*/,
|
||||
0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/,
|
||||
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
|
||||
0 /*splash_burst*/, -1 /*muzzle_seg*/,
|
||||
(int)damageData.damageType /*issue: autocannon = Ballistic, was Explosive*/);
|
||||
(int)damageData.damageType);
|
||||
|
||||
// task #61: mark this shot for REPLICATION so the peer sees the enemy's
|
||||
// cannon. ++fireCounter; the CALLER's Loaded case makes the two
|
||||
|
||||
Reference in New Issue
Block a user