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:
co-authored by
Claude Fable 5
parent
6988821525
commit
d78bde066d
+229
-170
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user