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
+18 -3
View File
@@ -47,9 +47,24 @@ converges on the target with NO aim/arc test (part_013.c:7758: `range <= weaponR
enemy. `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange = dist <
(1 hostZoneDamage) × weaponRange` — damage DEGRADES weapon reach; that flag is the HUD pip's
lit state. The 0x388 WRITER (the per-frame reticle→mech copy) is in an un-exported decomp gap.
**Port status:** acquisition is BRING-UP (target hardwired to `gEnemyMech` + a `BT_FIRE_ARC`
±30°+torso-reach cone gating trigger/damage — both PORT stand-ins, mech4.cpp targeting block);
the authentic slew+pick chain is NOT yet reconstructed. [T2 status / T0+T1 model]
**Port status: RECONSTRUCTED + runtime-verified (task #36, 2026-07-08) [T2].** The chain:
`Mech::targetReticle` (real `Reticle` member, bound to the TargetReticle attr 0x1d) ← slew from
the MOUSE (dev-box stand-in for the pod stick free-aim; `BT_AIM="x y"` pins it headless) →
`BTGetAimRay` (L4VIDEO; the ACTIVE eye publishes pos+LookAtRH basis via `BTSetAimCamera`, the
render loop publishes proj._22) → `Mech::PickRayHit` (world ray → local frame →
`BoundingBox::HitBy` slab test vs the collision template's ExtentBox → world hit point) → the
entity target slots. Designation is STICKY (persists off-crosshair; re-hover refreshes; cleared
when the target leaves the roster at burial). While HOT (crosshair on the hull) the impact point
= the PICKED point → STEP-6 resolves the zone under the crosshair (verified: center-aim at the
head band → zone 13 dominant); off-crosshair the converge point is center-mass chest. The HUD
draws the aim group at the slewed position ([0x9a] translate list, contained by push/pop), the
designator ring at the target's projected point (subB9 hot / subB8 designated,
`BTProjectToReticle`), and the edge arrows when off-screen/behind. `BT_FIRE_ARC` is now an
EXPLICIT OPT-IN presentation clamp (unset = authentic no-arc). The old hardwired lock, the
±30°-default cone, and the projectile path's gEnemyMech fallback are all REMOVED. LMB fires
lasers / RMB missiles (with SPACE/CTRL). ⚠ A view-selection bug was fixed en route: every
renderable rebuild stomped `mCamera` back to the chase eye (btl4vid `mViewInside` now persists
the chosen view — the aim camera feed and the V toggle both depend on it).
## Firing arc + muzzles
There is NO firing-arc field in the binary (`Emitter::FireWeapon` fires whenever HasActiveTarget) —
+4
View File
@@ -159,6 +159,10 @@ The authentic stack is DEFAULT-ON (`BTEnvOn`, mechrecon.hpp): `BT_GAIT_CUTOVER`,
| `BT_SECTOR_LOG` | SectorDisplay Make/Execute |
| `BT_NET_TRACE` | `[net-tx]/[net-rx]/[net-upd]` MP tracing |
| `BT_DEV_GAUGES` | render the 6 pod MFD surfaces in a separate dev window |
| `BT_AIM="x y"` | pin the reticle crosshair (reticle coords) — headless aim harness |
| `BT_AIM_LOG` | `[pick]` ray/box/hit diagnostics (Mech::PickRayHit) |
| `BT_FIRE_ARC=<deg>` | OPT-IN external-camera fire-arc clamp (unset = authentic no-arc) |
| `BT_START_INSIDE` | begin in the cockpit view (V toggles) |
Full render/locomotion gates (BT_RAMP, BT_MATPRI, BT_CULL, BT_SHADOW_*, BT_LODSEL, BT_ADDLOD,
BT_PUNCH, …) are catalogued in [[rendering]].
+9 -9
View File
@@ -51,15 +51,15 @@ authentic path scoped.
these are MODE flags — in the basic test mission the inert Myomers / dormant valve is likely AUTHENTIC
(advanced damage off); wiring makes them RESPOND when a mission enables the mode, not necessarily change
the basic-mission behavior. The MessageBoard feed is separate (StatusMessagePool, below).
- **Authentic target acquisition (the reticle pick-ray)** — the real chain is `Reticle`
(`engine/MUNGA/RETICLE.h` [T0], the mech's "TargetReticle" attr): slew `reticlePosition`
(mechmppr `freeAimSlew`) → engine pick-ray → `targetEntity`/`targetDamageZone`/`rayIntersection`
→ the mech's `0x37c/0x388/0x38c` slots (see [[combat-damage]] Targeting). Port bring-up
hardwires the lock to `gEnemyMech` + a `BT_FIRE_ARC` cone (both stand-ins). What resolves it:
reconstruct the slew input + the pick test (entity ray-intersection exists in the engine) + the
per-frame reticle→mech copy (its binary form is in an un-exported gap — re-export the
mech2/HUD region). Payoff: aimed zone damage, the designator box, authentic "keep the crosshair
on the enemy" gunnery. Status: OPEN (next combat-fidelity step).
- **Authentic target acquisition RECONSTRUCTED (task #36, 2026-07-08)** — the `Reticle`
pick-ray chain is LIVE (see [[combat-damage]] Targeting for the full port map): mouse slew →
pick ray → sticky designation → aimed zone damage + designator ring/arrows; `BT_FIRE_ARC` is
opt-in-only now. Residue: (a) the binary's own per-frame reticle→mech copy is still in an
un-exported gap (our writer follows RETICLE.h semantics [T3 dynamics]); (b) the pod's REAL slew
device channel (stick free-aim via the controls mapper / `freeAimSlew` @HUD+0x28C) is bridged
by the mouse — wire the real channel when pod hardware lands; (c) pre-burial, the pick still
tests the sinking wreck's collision box at its parked position (pre-existing wreck-targeting
behavior).
- **Cockpit HUD leftovers (task #35 residue, 2026-07-08)** — the reticle + 7 weapon pips are LIVE
(see [[gauges-hud]] + `phases/phase-02-dpl2d-reticle.md`), but three pieces remain: (a) the
**blx_cop canopy shell** renders BLACK (untextured/unlit in our inside view) so it's hidden by a
+103
View File
@@ -86,6 +86,102 @@ void BTSetLodEye(float x, float y, float z)
}
}
// ---------------------------------------------------------------------------
// AIM CAMERA FEED (task #36 authentic targeting): the ACTIVE eye publishes its
// world pose here (DPLEyeRenderable, the mCamera==this write site) and the
// render loop publishes the live projection's FOV term; the game side then
// derives the reticle PICK RAY (crosshair -> world, the engine Reticle's
// "pick point intersection" input) and the inverse projection (world ->
// reticle coords, the target-designator box position). Reticle coordinate
// convention = the dpl2d frame: centered origin, +y down, unit = half the
// viewport height (see dpl2d.cpp MapX/MapY).
// ---------------------------------------------------------------------------
static float gBTAimCamPos[3] = {0, 0, 0};
static float gBTAimCamX[3] = {1, 0, 0}; // camera right (LookAtRH xaxis)
static float gBTAimCamY[3] = {0, 1, 0}; // camera up (LookAtRH yaxis)
static float gBTAimCamZ[3] = {0, 0, 1}; // camera BACK (LookAtRH zaxis; view dir = -Z)
static float gBTAimTanHalf = 0.0f; // tan(fovY/2) = 1 / proj._22
static int gBTAimCamValid = 0;
void BTSetAimCamera(const float pos[3], const float xax[3],
const float yax[3], const float zax[3])
{
for (int i = 0; i < 3; ++i)
{
gBTAimCamPos[i] = pos[i];
gBTAimCamX[i] = xax[i];
gBTAimCamY[i] = yax[i];
gBTAimCamZ[i] = zax[i];
}
gBTAimCamValid = 1;
}
void BTSetAimProjection(float proj22)
{
if (proj22 > 1e-6f)
gBTAimTanHalf = 1.0f / proj22;
}
//
// Crosshair (reticle coords) -> world pick ray. RH camera looks down -Z;
// a point at reticle (rx, ry) sits at camera-space (rx*t, -ry*t, -1)
// (reticle +y is DOWN, camera +y is UP; t = tan(fovY/2), and the reticle's
// half-viewport-height unit cancels the aspect term exactly).
//
int BTGetAimRay(float rx, float ry, float outStart[3], float outDir[3])
{
if (!gBTAimCamValid || gBTAimTanHalf <= 0.0f)
return 0;
const float cx = rx * gBTAimTanHalf;
const float cy = -ry * gBTAimTanHalf;
float d[3];
float len = 0.0f;
for (int i = 0; i < 3; ++i)
{
d[i] = cx * gBTAimCamX[i] + cy * gBTAimCamY[i] - gBTAimCamZ[i];
len += d[i] * d[i];
}
len = sqrtf(len);
if (len < 1e-6f) return 0;
for (int i = 0; i < 3; ++i)
{
outStart[i] = gBTAimCamPos[i];
outDir[i] = d[i] / len;
}
return 1;
}
//
// World point -> reticle coords (the inverse; the designator box position).
// Returns 1 when the point is IN FRONT of the camera (rx/ry valid for
// on-screen placement); 0 when behind (rx still carries the correct SIDE
// sign so the caller can pick the left/right off-screen arrow).
//
int BTProjectToReticle(const float world[3], float *rx, float *ry)
{
if (!gBTAimCamValid || gBTAimTanHalf <= 0.0f)
{
*rx = 0.0f; *ry = 0.0f;
return 0;
}
float rel[3];
for (int i = 0; i < 3; ++i)
rel[i] = world[i] - gBTAimCamPos[i];
const float xc = rel[0]*gBTAimCamX[0] + rel[1]*gBTAimCamX[1] + rel[2]*gBTAimCamX[2];
const float yc = rel[0]*gBTAimCamY[0] + rel[1]*gBTAimCamY[1] + rel[2]*gBTAimCamY[2];
const float zc = rel[0]*gBTAimCamZ[0] + rel[1]*gBTAimCamZ[1] + rel[2]*gBTAimCamZ[2];
const float depth = -zc; // camera looks down -Z
if (depth < 0.1f)
{
*rx = (xc >= 0.0f) ? 2.0f : -2.0f; // side sign only
*ry = 0.0f;
return 0;
}
*rx = xc / (gBTAimTanHalf * depth);
*ry = -yc / (gBTAimTanHalf * depth);
return 1;
}
void BTPushBeamKind(float fx, float fy, float fz, float tx, float ty, float tz,
unsigned color, float ttl, float width, int kind)
{
@@ -7158,6 +7254,13 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
// the render lists
memset(mRenderLists, 0, sizeof(mRenderLists));
// aim-ray feed (task #36): publish the live projection's FOV term so the
// game side can build the reticle pick ray / designator projection.
{
extern void BTSetAimProjection(float proj22);
BTSetAimProjection(mProjectionMatrix._22);
}
HierarchicalDrawComponent *drawComp;
SChainIteratorOf<HierarchicalDrawComponent*> iterator(&mRenderables);
while ((drawComp = iterator.ReadAndNext()) != NULL)
+26
View File
@@ -5575,7 +5575,33 @@ void
// BOTH the cockpit and chase eyes; an unconditional write here let
// whichever eye executed LAST stomp the toggled camera every frame.
if (myRenderer->mCamera == this)
{
myDevice->SetTransform(D3DTS_VIEW, &view);
if (dbgEyeExec < 8)
DEBUG_STREAM << "[EYE] ACTIVE eye " << (void *)this
<< " (mCamera match) pos.y=" << pos.y << "\n" << std::flush;
// aim-ray feed (task #36): publish this eye's world pose --
// the LookAtRH basis (zaxis = back, view direction = -zaxis)
// -- for the reticle pick ray / designator projection.
{
extern void BTSetAimCamera(const float pos[3], const float xax[3],
const float yax[3], const float zax[3]);
D3DXVECTOR3 zax = pos - at;
D3DXVec3Normalize(&zax, &zax);
D3DXVECTOR3 xax;
D3DXVec3Cross(&xax, &up, &zax);
D3DXVec3Normalize(&xax, &xax);
D3DXVECTOR3 yax;
D3DXVec3Cross(&yax, &zax, &xax);
const float p[3] = { pos.x, pos.y, pos.z };
const float x3[3] = { xax.x, xax.y, xax.z };
const float y3[3] = { yax.x, yax.y, yax.z };
const float z3[3] = { zax.x, zax.y, zax.z };
BTSetAimCamera(p, x3, y3, z3);
}
}
myRenderer->GetMatrixStack()->Pop();
}
else if (dbgEyeExec < 8)
+61 -2
View File
@@ -616,7 +616,11 @@ HierarchicalDrawComponent*
debugOffset(3,0) = camPx; debugOffset(3,1) = camPy; debugOffset(3,2) = camPz; // W row (pos)
mEyeChase = new DPLEyeRenderable(entity, debugOffset, this_root, NULL);
mCamera = mEyeChase; // default view: chase (V toggles cockpit)
// Respect the pilot's CHOSEN view across renderable rebuilds: this build
// used to stomp mCamera back to chase on every remake (damage swaps,
// start-inside), silently flipping the active eye out from under the V
// toggle (and the aim-ray camera feed with it).
mCamera = (mViewInside && mEyeCockpit != 0) ? mEyeCockpit : mEyeChase;
DEBUG_STREAM << "[BTrender] external debug chase camera installed at ("
<< camPx << "," << camPy << "," << camPz << ") looking at ("
<< tgtX << "," << tgtY << "," << tgtZ << ") -- V toggles the cockpit eyepoint"
@@ -1064,8 +1068,17 @@ BTReticleRenderable::BTReticleRenderable(Entity *entity, Scalar *range_attr)
dpl2d_SetLineWidth(m, 1.0f);
dpl2d_FullScreenClip(m);
dpl2d_SetColor(m, 0.75f, 0.0f, 0.0f);
dpl2d_CallList(m, crossList); // [0xa1] target-box slot (empty until lock)
dpl2d_CallList(m, crossList); // [0xa1] target-box slot (empty until lock;
// Draw rebuilds it: designator ring at the
// target's screen point / off-screen arrows)
dpl2d_SetColor(m, 0.0f, 0.75f, 0.0f);
// the AIM GROUP (task #36): [0x9a] is the aim TRANSLATE -- CallList state
// persists to the caller (the dpl2d inline-include semantic), so the slew
// translate it carries positions the dot + crosses that follow. The
// PushState/PopState pair contains it so the fixed frame (tick ladders,
// tapes) stays put. [T3 -- the binary Execute is un-exported; mechanism
// per the engine ReticleRenderable's position-list pattern, T0.]
dpl2d_PushState(m);
dpl2d_CallList(m, aimDotList); // [0x9a] the aim translate
dpl2d_OpenPolypoint(m); // centre dot
dpl2d_AddPoint(m, 0.0f, 0.0f);
@@ -1083,6 +1096,7 @@ BTReticleRenderable::BTReticleRenderable(Entity *entity, Scalar *range_attr)
dpl2d_AddPoint(m, 0.0f, 0.10f); dpl2d_AddPoint(m, 0.0f, 0.16f);
dpl2d_AddPoint(m, 0.0f, -0.10f); dpl2d_AddPoint(m, 0.0f, -0.16f);
dpl2d_CloseLines(m);
dpl2d_PopState(m); // contain the aim translate
dpl2d_SetLineWidth(m, 1.0f);
// the RIGHT range ladder (13 ticks up the right side, dir 3 = -y travel)
BTReticleTickLadder(m, kRetTicksR, 1, kRetScaleY,
@@ -1222,6 +1236,49 @@ void
dpl2d_End(rangeCaretR);
dpl2d_Compile(rangeCaretR);
// the AIM translate [0x9a] (task #36): the slewed crosshair position, in
// reticle coordinates (fed by the mouse / stick free-aim in mech4).
{
extern float gBTAimX, gBTAimY;
Scalar t6[6] = { 1, 0, 0, 1, gBTAimX, gBTAimY };
dpl2d_Begin(aimDotList, 1);
dpl2d_ConcatMatrix(aimDotList, t6);
dpl2d_End(aimDotList);
dpl2d_Compile(aimDotList);
}
// the TARGET DESIGNATOR slot [0xa1] (task #36, Reticle::TargetDesignatorOn
// [T0]): the designated target's projected screen point gets the lock ring
// (subB9 ring+cross while the crosshair is ON it, subB8 plain ring while
// designated); off-screen / behind, the edge turn ARROW points the way back.
{
extern int gBTHudLockState; // 0 none / 1 designated / 2 hot
extern float gBTHudLockWorld[3];
extern int BTProjectToReticle(const float world[3], float *rx, float *ry);
dpl2d_Begin(crossList, 1);
if (gBTHudLockState != 0)
{
float tx = 0.0f, ty = 0.0f;
const int onScreen = BTProjectToReticle(gBTHudLockWorld, &tx, &ty);
const float xLimit = 1.55f; // past the ladders = off-screen
if (onScreen && tx > -xLimit && tx < xLimit && ty > -1.0f && ty < 1.0f)
{
Scalar d6[6] = { 1, 0, 0, 1, tx, ty };
dpl2d_PushState(crossList);
dpl2d_ConcatMatrix(crossList, d6);
dpl2d_CallList(crossList, (gBTHudLockState == 2) ? subB9 : subB8);
dpl2d_PopState(crossList);
}
else
{
dpl2d_CallList(crossList, (tx < 0.0f) ? leftArrow : rightArrow);
}
}
dpl2d_End(crossList);
dpl2d_Compile(crossList);
}
dpl2d_ExecuteList(masterList, device);
for (int i = 0; i < weaponCount; ++i)
@@ -1623,6 +1680,7 @@ BTL4VideoRenderer::BTL4VideoRenderer(
Check_Pointer(this);
mEyeCockpit = 0;
mEyeChase = 0;
mViewInside = 0;
}
//
@@ -1633,6 +1691,7 @@ BTL4VideoRenderer::BTL4VideoRenderer(
void
BTL4VideoRenderer::SetViewInside(int inside)
{
mViewInside = inside; // persists across renderable rebuilds
if (inside && mEyeCockpit != 0)
mCamera = mEyeCockpit;
else if (!inside && mEyeChase != 0)
+4
View File
@@ -661,6 +661,10 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
protected:
DPLEyeRenderable *mEyeCockpit; // the authentic cockpit eyepoint
DPLEyeRenderable *mEyeChase; // the external chase camera
int mViewInside; // the pilot's chosen view (V/state);
// survives renderable REBUILDS (the
// chase-eye build used to stomp
// mCamera back to chase every remake)
public:
//
+69 -1
View File
@@ -72,6 +72,7 @@
// AUTHENTIC GROUND MODEL ctor half (task #15): complete BoxedSolid type for the
// collisionTemplate/collisionVolume extent reads + the template bottom lift.
#include <BOXSOLID.hpp>
#include <line.hpp> // Line/Ray -- the PickRayHit slab test (task #36)
// STEP 6 cylinder hit-location table. dmgtable.hpp pulls in no subsystem
// headers (only Plug + mechrecon + <vector>), so it is safe here -- unlike the
// real subsystem headers, whose classes collide with mech.cpp's local stubs.
@@ -435,6 +436,67 @@ Point3D
return local;
}
//
// Ray pick against this mech (task #36 -- the engine Reticle's "pick point
// intersection testing"): transform the world ray into the mech's local frame
// (rotation part only for the direction), slab-test it against the collision
// template's ExtentBox via the engine's own BoundingBox::HitBy (which clips
// the Line's length to the entry distance), and return the world hit point.
// The same collision volume the movement/collision code uses; the hit point
// then drives the cylinder hit-location table at damage delivery (aimed
// shots resolve to the zone under the crosshair).
//
Logical
Mech::PickRayHit(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world)
{
BoxedSolid *tmpl = GetCollisionTemplate();
if (tmpl == 0)
return False;
// world -> local: point via the full inverse, direction via the inverse
// rotation (transform a second point and difference -- exact for the
// orthonormal localToWorld, no scale).
Point3D localStart;
localStart.MultiplyByInverse(start, localToWorld);
Point3D worldTip(start.x + dir.x, start.y + dir.y, start.z + dir.z);
Point3D localTip;
localTip.MultiplyByInverse(worldTip, localToWorld);
Vector3D localDir(localTip.x - localStart.x,
localTip.y - localStart.y,
localTip.z - localStart.z);
Scalar dlen = (Scalar)Sqrt(localDir.x*localDir.x + localDir.y*localDir.y
+ localDir.z*localDir.z);
if (dlen < 1e-6f)
return False;
localDir.x /= dlen; localDir.y /= dlen; localDir.z /= dlen;
BoundingBox box(*(const ExtentBox *)tmpl); // the template's extents
Line ray(localStart, UnitVector(localDir.x, localDir.y, localDir.z), max_range);
const Logical hitOK = box.HitBy(&ray); // clips ray.length to the entry
if (BTEnvOn("BT_AIM_LOG", 0))
{
static int s_n = 0;
if ((++s_n % 60) == 1)
DEBUG_STREAM << "[pick] box=(" << tmpl->minX << ".." << tmpl->maxX
<< ", " << tmpl->minY << ".." << tmpl->maxY
<< ", " << tmpl->minZ << ".." << tmpl->maxZ
<< ") lstart=(" << localStart.x << "," << localStart.y << "," << localStart.z
<< ") ldir=(" << localDir.x << "," << localDir.y << "," << localDir.z
<< ") hit=" << (int)hitOK << " len=" << ray.length << "\n" << std::flush;
}
if (!hitOK)
return False;
if (hit_world != 0)
{
Point3D localHit;
ray.Project(ray.length, &localHit); // start + length*dir (local)
hit_world->Multiply(localHit, localToWorld); // local -> world
}
return True;
}
//
// The roster torso (sinkSourceSubsystem @0x438, ClassID 0xBC5) -- parity with
// the binary's *(mech+0x438).
@@ -532,7 +594,7 @@ const Mech::IndexEntry
ATTRIBUTE_ENTRY(Mech, CurrentSpeed, legCycleSpeed), // 0x1a (existing @0x348)
ATTRIBUTE_ENTRY(Mech, MaxRunSpeed, reverseStrideLength), // 0x1b (existing @0x34c = run/top speed)
ATTRIBUTE_ENTRY(Mech, EyepointRotation, eyepointRotation), // 0x1c (real member -- the eye reads it per frame)
ATTRIBUTE_ENTRY(Mech, TargetReticle, attrPad), // 0x1d
ATTRIBUTE_ENTRY(Mech, TargetReticle, targetReticle), // 0x1d (real Reticle struct -- task #36)
ATTRIBUTE_ENTRY(Mech, FootStep, attrPad), // 0x1e
ATTRIBUTE_ENTRY(Mech, AnimationState, attrPad), // 0x1f
ATTRIBUTE_ENTRY(Mech, ReplicantAnimationState, attrPad), // 0x20
@@ -681,6 +743,12 @@ Mech::Mech(
weaponRoster.Construct(0); // FUN_004a4df6(this+0x1ef)
damageableSubsystems.Construct(0); // FUN_004a4e41(this+499)
// The target reticle (task #36): armed + pick-testing. The Reticle ctor
// zeroes position/target; the pilot's slew + the pick feed it per frame.
targetReticle.reticleState = Reticle::ReticleOn;
targetReticle.pickPointingOn = True;
targetReticle.reticleElementMask = Reticle::AllEnabledGroup;
for (int i = 0; i < 5; ++i) // FUN_0043ad4f x5
{
telemetryFilter[i].Initialize(15, 0.0f); // this[0x1f8..0x204]
+18
View File
@@ -53,6 +53,7 @@
#include <affnmtrx.hpp> // AffineMatrix
#include <average.hpp> // AverageOf<T>
#include <scalar.hpp> // Scalar
#include <reticle.hpp> // Reticle (the TargetReticle attribute struct)
#include "mechrecon.hpp" // reconstruction shim (proxies, artifact globals, type aliases)
@@ -469,6 +470,23 @@ public:
Scalar standingTemplateMaxY; // binary @0x518 collisionTemplate->maxY at ctor
Scalar duckedTemplateMaxY; // binary @0x51c 0.6 x standing (duck preset)
Scalar templateBottomLift; // binary @0x4b8 0.05 x (volume maxX-minX)
// AUTHENTIC TARGETING (task #36): the engine Reticle struct (MUNGA/RETICLE.h
// [T0]) -- the mech's "TargetReticle" attribute (id 0x1d) binds to it, per the
// RP VTV analog (VTV.h: `Reticle targetReticle` + TargetReticleAttributeID).
// The pilot slews reticlePosition; the pick ray through it fills
// rayIntersection / targetEntity / targetDamageZone; the mech's engine-Entity
// target slots (+0x37c/+0x388/+0x38c) are fed from this each frame (the
// binary's writer is in an un-exported gap -- semantics per RETICLE.h).
// By-name access only; declared after the layout-locked fields.
Reticle targetReticle;
// Ray pick against this mech (the Reticle "pick point intersection"):
// world ray -> local frame -> slab test vs the collision template's
// ExtentBox (BoundingBox::HitBy clips the Line at the entry distance)
// -> world hit point. Defined in mech.cpp.
Logical PickRayHit(const Point3D &start, const Vector3D &dir,
Scalar max_range, Point3D *hit_world);
protected:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+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