Authentic target acquisition LIVE: reticle slew + pick-ray lock + aimed zone damage (task #36)

The engine Reticle model (MUNGA/RETICLE.h [T0]) reconstructed end to end:
- Mech::targetReticle is a real Reticle member bound to the TargetReticle
  attribute (0x1d), per the RP VTV analog (VTV.h targetReticle).
- Crosshair slew: mouse -> client rect -> reticle coords (the pod stick
  free-aim channel's dev-box stand-in); BT_AIM="x y" pins it headless.
  LMB fires lasers / RMB missiles (alongside SPACE/CTRL).
- Pick ray: the ACTIVE eye publishes pos + LookAtRH basis (BTSetAimCamera,
  L4VIDRND view-write site) + the render loop publishes proj._22;
  BTGetAimRay builds the world ray, Mech::PickRayHit slab-tests it against
  the collision template's ExtentBox via the engine's BoundingBox::HitBy
  (local frame; clips the Line at entry) -> world hull point.
- Designation: the mech under the crosshair designates (sticky; re-hover
  refreshes; cleared when the target leaves the roster at burial); the
  entity target slots 0x37c/0x388/0x38c feed the whole weapon path.
- Aimed fire: while HOT the impact point is the PICKED hull point -> the
  STEP-6 cylinder lookup resolves the zone under the crosshair (verified:
  center-aim -> head-band zone 13 dominant). Off-crosshair the sticky
  designation converges on center mass.
- HUD: the aim group draws at the slewed position ([0x9a] translate,
  contained by push/pop); the designator ring tracks the target's
  projected point (subB9 hot / subB8 designated, BTProjectToReticle);
  edge arrows when off-screen/behind.
- AUTHENTIC gating: no fire arc exists in the binary (FireWeapon fires
  whenever HasActiveTarget, part_013.c:7758) -> BT_FIRE_ARC is now an
  explicit OPT-IN presentation clamp; the hardwired gEnemyMech lock and
  the projectile path's gEnemyMech fallback are removed.
- Fixed en route: every renderable rebuild stomped mCamera back to the
  chase eye (start-inside silently lost the cockpit camera; the aim feed
  exposed it). BTL4VideoRenderer::mViewInside persists the chosen view.

Verified headless: BT_AIM="0 0" -> HOT lock, pick hits the hull face at
exact range, aimed zones resolve; BT_AIM="0.8 0.3" -> no lock, zero
damage, zero missile launches; kill chain completes to wreck + smoke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 20:57:11 -05:00
co-authored by Claude Fable 5
parent 6988821525
commit d78bde066d
10 changed files with 541 additions and 185 deletions
+229 -170
View File
@@ -543,6 +543,19 @@ static const Scalar kWeaponRange = 100.0f; // bring-up "in range" threshold fo
// Single local-player drive state (bring-up).
static Scalar gDriveHeading = 0.0f; // yaw about world up (Y)
// AUTHENTIC TARGETING (task #36): the reticle slew state, in reticle/dpl2d
// coordinates (centered origin, +y down, unit = half viewport height). The
// pod slewed this with the stick free-aim channel; the dev box uses the MOUSE
// (absolute cursor -> client -> reticle) or the BT_AIM="x y" harness. Read by
// the HUD Draw (crosshair position) and the pick-ray each sim frame.
float gBTAimX = 0.0f;
float gBTAimY = 0.0f;
// The HUD designator feed (mech4 -> btl4vid Draw): the designated target's
// world chest point + the lock state (0 = none, 1 = designated, 2 = HOT --
// the crosshair is ON it right now).
int gBTHudLockState = 0;
float gBTHudLockWorld[3] = { 0, 0, 0 };
// BT_GOTO beeline harness outputs (consumed by the mapper bridge, mechmppr.cpp)
int gBTGotoActive = 0;
float gBTGotoTurn = 0.0f;
@@ -765,17 +778,15 @@ void
}
}
// Bring-up: the mech's own target slot (owner+0x388) isn't populated (the visible
// fire path targets the gEnemyMech global directly), so fall back to it here.
// (The passed targetPos comes from MECH_TARGET_POS, which the targeting block
// now writes at TORSO height -- the fallback lifts to match.)
// AUTHENTIC (task #36): NO fallback target. The launcher passes the mech's
// own designated-target slots (owner+0x388 via GetTargetPosition); with no
// designation the missile does not launch -- the acquisition (crosshair on
// the enemy -> pick -> designate) is the ONLY route to a target, exactly
// like the energy weapons. (The old gEnemyMech fallback pre-dated the
// acquisition and let missiles bypass it.)
Point3D tpos = targetPos;
if ((target == 0 || tpos.x == 0.0f) && gEnemyMech != 0)
{
target = gEnemyMech;
tpos = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
tpos.y += kMuzzleHeight;
}
if (target == 0)
return;
Vector3D d;
d.x = tpos.x - mz.x; d.y = tpos.y - mz.y; d.z = tpos.z - mz.z;
Scalar len = (Scalar)sqrtf(d.x*d.x + d.y*d.y + d.z*d.z);
@@ -1199,10 +1210,13 @@ void
gBTDrive.keyLeft = focused && ((pAsync('A') | pAsync(0x25 /*VK_LEFT*/)) & dn) ? 1 : 0;
gBTDrive.keyRight = focused && ((pAsync('D') | pAsync(0x27 /*VK_RIGHT*/)) & dn) ? 1 : 0;
// SPLIT WEAPON CONTROLS (interim, toward the real controls-mapper
// weapon groups): SPACE = the energy weapons (lasers), CTRL = the
// missile launchers. (Both used to share one fire-everything key.)
gBTDrive.fire = focused && (pAsync(0x20 /*VK_SPACE*/) & dn) ? 1 : 0;
gBTMissileKey = focused && (pAsync(0x11 /*VK_CONTROL*/) & dn) ? 1 : 0;
// weapon groups): SPACE / LMB = the energy weapons (lasers),
// CTRL / RMB = the missile launchers. (The mouse buttons pair
// with the mouse-slewed reticle -- task #36.)
gBTDrive.fire = focused && ((pAsync(0x20 /*VK_SPACE*/) |
pAsync(0x01 /*VK_LBUTTON*/)) & dn) ? 1 : 0;
gBTMissileKey = focused && ((pAsync(0x11 /*VK_CONTROL*/) |
pAsync(0x02 /*VK_RBUTTON*/)) & dn) ? 1 : 0;
static int sPrevX = 0;
const int xNow = focused && (pAsync('X') & dn) ? 1 : 0;
if (xNow && !sPrevX) gBTDrive.allStop = 1; // edge -> one all-stop
@@ -1218,6 +1232,62 @@ void
BTSetViewInside(sViewInside);
}
sPrevV = vNow;
// RETICLE SLEW (task #36): the pod slewed the target reticle
// with the stick free-aim channel; the dev-box stand-in is the
// MOUSE -- absolute cursor position mapped through the client
// rect into reticle coordinates (centered, +y down, unit =
// half the client height -- the dpl2d frame). BT_AIM="x y"
// pins the crosshair for headless harness runs.
{
static int sAimEnv = -1;
static float sAimEnvX = 0.0f, sAimEnvY = 0.0f;
if (sAimEnv < 0)
{
const char *av = getenv("BT_AIM");
sAimEnv = (av != 0 &&
sscanf(av, "%f %f", &sAimEnvX, &sAimEnvY) == 2) ? 1 : 0;
}
if (sAimEnv)
{
gBTAimX = sAimEnvX;
gBTAimY = sAimEnvY;
}
else if (focused && pFg != 0)
{
typedef int (__stdcall *CurFn)(POINT *);
typedef int (__stdcall *StcFn)(void *, POINT *);
typedef int (__stdcall *CrFn)(void *, RECT *);
static CurFn pCur = 0; static StcFn pStc = 0; static CrFn pCr = 0;
if (pCur == 0)
{
HMODULE u = GetModuleHandleA("user32.dll");
pCur = (CurFn)GetProcAddress(u, "GetCursorPos");
pStc = (StcFn)GetProcAddress(u, "ScreenToClient");
pCr = (CrFn)GetProcAddress(u, "GetClientRect");
}
void *wnd = pFg(); // focused == our window
POINT cp; RECT rc;
if (pCur && pStc && pCr && wnd != 0 &&
pCur(&cp) && pStc(wnd, &cp) && pCr(wnd, &rc) &&
rc.right > rc.left && rc.bottom > rc.top)
{
const float w = (float)(rc.right - rc.left);
const float h = (float)(rc.bottom - rc.top);
const float hh = h * 0.5f;
float rx = ((float)cp.x - w * 0.5f) / hh;
float ry = ((float)cp.y - h * 0.5f) / hh;
// clamp to the visible frame (aspect-wide in x)
const float xmax = (w / h);
if (rx < -xmax) rx = -xmax;
if (rx > xmax) rx = xmax;
if (ry < -1.0f) ry = -1.0f;
if (ry > 1.0f) ry = 1.0f;
gBTAimX = rx;
gBTAimY = ry;
}
}
}
}
}
@@ -2068,128 +2138,49 @@ void
}
}
// --- WEAPON FIRE VISUAL (port addition): on the trigger, draw the muzzle->hit
// BEAM + a muzzle flash + (solo) an impact explosion -- ALWAYS, so you can
// see your weapons fire even with no locked target (aim = the crosshair,
// straight ahead, raycast to the terrain). The 1995 game rendered
// cockpit-only so it never needed a third-person weapon view; our external
// camera does, and the dpl_* beam layer was never ported -- this is it.
// Damage stays in the target block below; this owns only the visual, on its
// own cooldown. (Real per-weapon fire -- Emitter::FireWeapon heat/charge --
// runs in the subsystem tick when a target is locked.)
// FIRING ARC: weapons fire where the mech can POINT its guns -- a target it
// cannot bring the guns to bear on is NOT hit (no shooting the enemy out of
// your back). The AUTHENTIC, data-driven traverse is the TORSO twist range:
// weapon .SUB resources carry NO arc field, so what lets a mech aim off
// dead-ahead is the torso mount (Torso::GetHorizontalReach, the wider software
// twist limit). The Blackhawk's torso is fixed-forward (TorsoHorizontalEnabled
// =0 -> reach 0), so its guns line up with the mech facing; a twist-capable
// mech widens the arc by its real torso limit. On top of the torso reach we
// keep a base aim/convergence tolerance (BT_FIRE_ARC degrees, default 30) --
// the reticle box, a port presentation parameter for the external camera the
// 1995 cockpit view never needed. In-arc: the beam converges on the target
// and the shot hits; out-of-arc: the guns fire straight ahead, nothing is hit.
bool targetInArc = false;
// FIRING ARC -- now an EXPLICIT OPT-IN presentation clamp only (task #36).
// AUTHENTIC: the binary has NO aim/arc test -- Emitter::FireWeapon engages
// the LOCKED target whenever HasActiveTarget (part_013.c:7758); the skill
// is ACQUIRING the lock (crosshair on the enemy -> pick ray -> designate).
// The old ±30°-default cone was a stand-in from before the acquisition
// existed. Set BT_FIRE_ARC=<degrees> to re-enable the cone (+ the mech's
// real torso reach) as an external-camera presentation clamp; unset = the
// authentic no-arc model.
bool targetInArc = true;
{
UnitVector zAxisA;
localToWorld.GetFromAxis(Z_Axis, &zAxisA);
Vector3D fA(-(Scalar)zAxisA.x, -(Scalar)zAxisA.y, -(Scalar)zAxisA.z);
Scalar fl = (Scalar)Sqrt(fA.x*fA.x + fA.y*fA.y + fA.z*fA.z);
if (fl < 1e-4f) fl = 1.0f;
fA.x /= fl; fA.y /= fl; fA.z /= fl;
if (gEnemyMech != 0)
static Scalar s_baseRad = -2.0f;
if (s_baseRad < -1.5f)
{
const char *av = getenv("BT_FIRE_ARC");
s_baseRad = (av != 0) ?
(Scalar)((double)atof(av) * 3.14159265358979 / 180.0) : -1.0f;
}
if (s_baseRad >= 0.0f && gEnemyMech != 0)
{
UnitVector zAxisA;
localToWorld.GetFromAxis(Z_Axis, &zAxisA);
Vector3D fA(-(Scalar)zAxisA.x, -(Scalar)zAxisA.y, -(Scalar)zAxisA.z);
Scalar fl = (Scalar)Sqrt(fA.x*fA.x + fA.y*fA.y + fA.z*fA.z);
if (fl < 1e-4f) fl = 1.0f;
fA.x /= fl; fA.y /= fl; fA.z /= fl;
Point3D ep = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
Vector3D toE(ep.x - localOrigin.linearPosition.x,
ep.y - localOrigin.linearPosition.y,
ep.z - localOrigin.linearPosition.z);
Scalar tl = (Scalar)Sqrt(toE.x*toE.x + toE.y*toE.y + toE.z*toE.z);
if (tl < 1e-4f) tl = 1.0f;
Scalar d = (fA.x*toE.x + fA.y*toE.y + fA.z*toE.z) / tl; // cos(angle to target)
// base aim tolerance (radians) -- read once from BT_FIRE_ARC (degrees).
static Scalar s_baseRad = -1.0f;
if (s_baseRad < 0.0f)
{
const char *av = getenv("BT_FIRE_ARC");
Scalar deg = av ? (Scalar)atof(av) : 30.0f;
s_baseRad = (Scalar)((double)deg * 3.14159265358979 / 180.0);
}
// effective half-arc = base tolerance + this mech's real torso reach.
Scalar d = (fA.x*toE.x + fA.y*toE.y + fA.z*toE.z) / tl;
Scalar half = s_baseRad + GetHorizontalFiringReach();
if (half > 3.14159265f) half = 3.14159265f; // clamp: full hemisphere+
if (half > 3.14159265f) half = 3.14159265f;
targetInArc = (d >= (Scalar)cos((double)half));
}
}
{
UnitVector zAxisF;
localToWorld.GetFromAxis(Z_Axis, &zAxisF);
Vector3D fwd(-(Scalar)zAxisF.x, -(Scalar)zAxisF.y, -(Scalar)zAxisF.z); // mech faces -Z
Scalar flen = (Scalar)Sqrt(fwd.x*fwd.x + fwd.y*fwd.y + fwd.z*fwd.z);
if (flen < 1e-4f) flen = 1.0f;
fwd.x /= flen; fwd.y /= flen; fwd.z /= flen;
// AUTHENTIC MUZZLES: fire from the mech's real gun-port SITE segments
// (BLH.SKL: siter/lu/dgunport on jointrgun/jointlgun -- the arm guns),
// resolved to world by (segmentToEntity x localToWorld). Beams then
// emit from the actual arm guns and track them as the mech moves/animates,
// instead of one point above the torso. Falls back to a centre muzzle for
// a mech with no resolvable gun ports.
Point3D muzzles[8];
int nMuz = 0;
static const char *const kGunPorts[] =
{ "siterugunport", "sitelugunport", "siterdgunport", "siteldgunport",
"siterbgunport", "sitelbgunport" };
for (int gp = 0; gp < 6 && nMuz < 8; ++gp)
{
EntitySegment *seg = GetSegment(CString(kGunPorts[gp]));
if (seg != 0)
{
AffineMatrix mw;
mw.Multiply(seg->GetSegmentToEntity(), localToWorld);
muzzles[nMuz] = mw; // Point3D = matrix translation (W_Axis)
++nMuz;
}
}
if (nMuz == 0)
{
Point3D c = localOrigin.linearPosition;
c.x += fwd.x * kMuzzleForward;
c.y += kMuzzleHeight + fwd.y * kMuzzleForward;
c.z += fwd.z * kMuzzleForward;
muzzles[0] = c; nMuz = 1;
}
const Point3D &muzzle = muzzles[0]; // aim-raycast origin
// converge on the enemy ONLY when it's within the forward firing arc;
// otherwise the guns fire straight ahead (crosshair raycast), never back.
const bool haveEnemy = (gEnemyMech != 0 && targetInArc);
Point3D aim;
if (haveEnemy)
{
aim = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
aim.y += kMuzzleHeight; // aim at the torso, not the feet
}
else
{
// default aim = straight ahead at max range; the terrain raycast
// (BTGroundRayHit marches SampleBand over every map instance -- a
// real per-frame cost on dense maps) only runs when a shot actually
// needs the impact point: trigger down or a discharge in flight.
aim.x = muzzle.x + fwd.x * kWeaponRange;
aim.y = muzzle.y + fwd.y * kWeaponRange;
aim.z = muzzle.z + fwd.z * kWeaponRange;
if (gBTDrive.fireForced || gBTDrive.fire || gBeamCooldown > 0.0f)
{
extern bool BTGroundRayHit(float,float,float, float,float,float,
float, float*,float*,float*);
float hx, hy, hz;
if (BTGroundRayHit(muzzle.x, muzzle.y, muzzle.z, fwd.x, fwd.y, fwd.z,
kWeaponRange, &hx, &hy, &hz))
{ aim.x = hx; aim.y = hy; aim.z = hz; }
}
}
// (The old fwd/muzzle-collection + straight-ahead `aim` block that
// lived here fed the pre-#33 single-visual beam; the per-weapon
// emitter beams below carry their own live muzzle + endpoint, so it
// was dead code and was removed with the task-#36 acquisition work.)
// resolve the "explode" effect once (also used by the target block).
if (gExplodeReady == 0)
@@ -2307,37 +2298,94 @@ void
}
}
// --- TARGETING (bring-up): lock the player's current target onto the
// spawned enemy mech. This writes the three mech target slots that the
// weapon/fire path reads (target position @0x37c, target entity @0x388,
// targeted-subsystem index @0x38c) so HasActiveTarget() becomes true and
// the beam/aim has a world point. Real acquisition -- nearest-enemy scan
// or the pilot "hotbox" select through the sensor subsystem -- is the next
// refinement; here we lock the one known target. The enemy is stationary
// (drive gated to the player), but we read its LIVE origin so this stays
// correct once it can move.
// --- TARGETING (task #36 -- the AUTHENTIC Reticle acquisition,
// engine/MUNGA/RETICLE.h [T0]): the pilot slews the crosshair
// (gBTAimX/Y); a pick ray through it tests the enemy's collision
// volume; the mech UNDER the crosshair becomes the designated target
// (sticky -- it persists when the crosshair drifts off, marked by
// the HUD designator; re-hover to refresh, burial clears it). The
// designation feeds the engine-Entity target slots the whole weapon
// path reads (target position @0x37c, target entity @0x388, zone
// @0x38c). While the crosshair is ON the target ("hot"), the aim
// point is the PICKED HULL POINT -- the STEP-6 cylinder lookup then
// resolves damage to the zone under the crosshair (aimed fire).
Entity *hotTarget = 0; // mech under the crosshair NOW
Point3D hotPoint; // picked world point on its hull
{
extern int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3]);
float rs[3], rd[3];
if (BTGetAimRay(gBTAimX, gBTAimY, rs, rd) && gEnemyMech != 0)
{
Point3D rayStart(rs[0], rs[1], rs[2]);
Vector3D rayDir(rd[0], rd[1], rd[2]);
if (((Mech *)gEnemyMech)->PickRayHit(rayStart, rayDir, 4000.0f, &hotPoint))
hotTarget = gEnemyMech;
}
// The Reticle struct (the mech's TargetReticle attribute): position,
// pick result. targetDamageZone stays -1 -- the zone ROLL happens at
// damage delivery (the authentic percent-table roll, STEP 6).
targetReticle.reticlePosition.x = gBTAimX;
targetReticle.reticlePosition.y = gBTAimY;
targetReticle.targetEntity = hotTarget;
targetReticle.targetDamageZone = -1;
if (hotTarget != 0)
targetReticle.rayIntersection = hotPoint;
if (hotTarget != 0)
{
MECH_TARGET_ENTITY(this) = hotTarget; // designate (sticky)
MECH_TARGET_SUBIDX(this) = -1;
MECH_TARGET_POS(this) = hotPoint; // the aimed hull point
}
else if (MECH_TARGET_ENTITY(this) != 0)
{
if (MECH_TARGET_ENTITY(this) != gEnemyMech)
{
// the designated mech left the target roster (wreck buried /
// entity gone): drop the designation.
MECH_TARGET_ENTITY(this) = 0;
MECH_TARGET_SUBIDX(this) = -1;
}
else
{
// crosshair off the mech: the designation persists; the
// converge point tracks the target's center mass (chest).
Mech *des = (Mech *)MECH_TARGET_ENTITY(this);
Point3D cm = des->localOrigin.linearPosition;
cm.y += kMuzzleHeight;
MECH_TARGET_POS(this) = cm;
}
}
}
// HUD feeds: the range caret + the designator box (world point + state).
{
extern void BTSetHudTargetRange(Scalar range);
Entity *des = MECH_TARGET_ENTITY(this);
if (des != 0)
{
Point3D dp = ((Mech *)des)->localOrigin.linearPosition;
float hddx = dp.x - localOrigin.linearPosition.x;
float hddz = dp.z - localOrigin.linearPosition.z;
BTSetHudTargetRange((Scalar)sqrtf(hddx*hddx + hddz*hddz));
gBTHudLockState = (hotTarget != 0) ? 2 : 1;
gBTHudLockWorld[0] = dp.x;
gBTHudLockWorld[1] = dp.y + kMuzzleHeight;
gBTHudLockWorld[2] = dp.z;
}
else
{
BTSetHudTargetRange(1200.0f); // no target: the binary default
gBTHudLockState = 0; // (0x44960000 @part_013.c:5637)
}
}
if (gEnemyMech != 0)
{
Point3D enemyPos = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
// The AIM POINT is the target's torso, not its origin (ground level
// between the feet): every consumer of the target slot -- the weapon
// beams' endpoints (GetTargetPosition), the missiles, the reticle --
// wants the center-mass point the pilot is actually aiming at.
Point3D aimPos = enemyPos;
aimPos.y += kMuzzleHeight;
MECH_TARGET_POS(this) = aimPos; // mech+0x37c
MECH_TARGET_ENTITY(this) = gEnemyMech; // mech+0x388 (HasActiveTarget gate)
MECH_TARGET_SUBIDX(this) = -1; // mech+0x38c whole-mech target
// feed the HUD's range caret (the reticle's right-ladder pointer)
{
extern void BTSetHudTargetRange(Scalar range);
float hddx = enemyPos.x - localOrigin.linearPosition.x;
float hddz = enemyPos.z - localOrigin.linearPosition.z;
BTSetHudTargetRange((Scalar)sqrtf(hddx*hddx + hddz*hddz));
}
const int designated = (MECH_TARGET_ENTITY(this) != 0);
float ddx = enemyPos.x - localOrigin.linearPosition.x;
float ddy = enemyPos.y - localOrigin.linearPosition.y;
@@ -2348,11 +2396,11 @@ void
if (gTargetLogAccum >= 1.0f)
{
gTargetLogAccum = 0.0f;
DEBUG_STREAM << "[target] locked enemy entity=" << (void *)gEnemyMech
DEBUG_STREAM << "[target] aim=(" << gBTAimX << "," << gBTAimY << ")"
<< (hotTarget != 0 ? " HOT (crosshair on target)"
: (designated ? " designated (off-crosshair)" : " no target"))
<< " range=" << range
<< (range <= kWeaponRange ? " IN RANGE (ready to fire)" : " (closing)")
<< (targetInArc ? " [in arc -> converge]" : " [out of arc -> fire forward, no hit]")
<< " torsoReach=" << GetHorizontalFiringReach() << "rad"
<< (range <= kWeaponRange ? " IN RANGE" : "")
<< "\n" << std::flush;
}
@@ -2370,12 +2418,13 @@ void
// E8: pulse the weapon trigger per frame (1,0,1,0...) so the real Emitter's
// CheckFireEdge sees clean rising edges for repeated auto-fire. Read by
// EmitterSimulation; only the player's emitter (with a target) actually fires.
// EmitterSimulation. AUTHENTIC gating: the trigger passes through and the
// weapon's OWN HasActiveTarget gate decides (Emitter::FireWeapon fires only
// with a designated target -- part_013.c:7758; no aim/arc test exists).
// targetInArc is the explicit BT_FIRE_ARC presentation clamp (default true).
extern int gBTWeaponTrigger;
// the real Emitter only fires at a target in the forward arc -- no
// heat/damage out of arc (you can't shoot the enemy behind you).
gBTWeaponTrigger = (fireWanted && targetInArc) ? (gBTWeaponTrigger ? 0 : 1) : 0;
// the missile channel: CTRL (or the autofire harness) -- same edge pulse
// the missile channel: CTRL / RMB (or the autofire harness) -- same edge pulse
extern int gBTMissileTrigger;
const int missileWanted = gBTDrive.fireForced || gBTMissileKey;
gBTMissileTrigger = (missileWanted && targetInArc) ? (gBTMissileTrigger ? 0 : 1) : 0;
@@ -2403,25 +2452,35 @@ void
}
}
if (fireWanted && targetInArc && gFireCooldown <= 0.0f && range <= kWeaponRange
&& gExplodeReady == 1)
if (fireWanted && designated && targetInArc && gFireCooldown <= 0.0f
&& range <= kWeaponRange && gExplodeReady == 1)
{
gFireCooldown = kFireCooldown;
++gShotCount;
// Beam entry point: on the enemy's surface toward the shooter, at
// the beam's convergence height. (Exactly the axis point would be
// angularly degenerate for the cylinder sector lookup.) Used for
// BOTH the hit-explosion position and the unaimed damage dispatch --
// the effect fires where the beam lands (chest height, facing side),
// not at the mech origin (ground level between the feet).
Point3D impact = enemyPos;
if (range > 1e-3f)
// Impact point (task #36): while the crosshair is ON the target the
// shot lands at the PICKED HULL POINT -- the STEP-6 cylinder lookup
// resolves damage to the zone under the crosshair (aimed fire: leg
// shots hit legs, torso shots hit the torso). Off-crosshair (the
// sticky designation converging), the shot enters on the surface
// toward the shooter at chest height (the unaimed synth point --
// exactly the axis point would be angularly degenerate for the
// cylinder sector lookup).
Point3D impact;
if (hotTarget != 0)
{
impact.x -= (ddx / range) * 3.0f; // ~torso radius toward shooter
impact.z -= (ddz / range) * 3.0f;
impact = hotPoint;
}
else
{
impact = enemyPos;
if (range > 1e-3f)
{
impact.x -= (ddx / range) * 3.0f; // ~torso radius toward shooter
impact.z -= (ddz / range) * 3.0f;
}
impact.y += kMuzzleHeight; // chest height (beam aim height)
}
impact.y += kMuzzleHeight; // chest height (beam aim height)
Origin exp_origin = ((Mech *)gEnemyMech)->localOrigin;
exp_origin.linearPosition = impact; // at the hit point