diff --git a/context/combat-damage.md b/context/combat-damage.md index 67a8432..3ee0725 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -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) — diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 0cddb36..974835f 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -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=` | 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]]. diff --git a/context/open-questions.md b/context/open-questions.md index d63f611..8fa21be 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -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 diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 0d3739f..5e68173 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -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 iterator(&mRenderables); while ((drawComp = iterator.ReadAndNext()) != NULL) diff --git a/engine/MUNGA_L4/L4VIDRND.cpp b/engine/MUNGA_L4/L4VIDRND.cpp index bc728e2..b6a6322 100644 --- a/engine/MUNGA_L4/L4VIDRND.cpp +++ b/engine/MUNGA_L4/L4VIDRND.cpp @@ -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) diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index bd5a3aa..3f2988d 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -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) diff --git a/game/reconstructed/btl4vid.hpp b/game/reconstructed/btl4vid.hpp index 0e4761f..319ec0a 100644 --- a/game/reconstructed/btl4vid.hpp +++ b/game/reconstructed/btl4vid.hpp @@ -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: // diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index be03fb6..d4a0746 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -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 +#include // Line/Ray -- the PickRayHit slab test (task #36) // STEP 6 cylinder hit-location table. dmgtable.hpp pulls in no subsystem // headers (only Plug + mechrecon + ), 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] diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index f37d91f..fe853d5 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -53,6 +53,7 @@ #include // AffineMatrix #include // AverageOf #include // Scalar +#include // 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: //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 3ab1131..dda4e3c 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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= 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