aimed fire strikes the part under the crosshair: the per-segment pick (#73)
The port's target pick was a whole-mech bounding-box slab test, and every hit -- aimed or not -- dispatched zone -1 into the victim's cylinder lottery. The recovered 1995 model (the division-card scene intersection) struck a SEGMENT and credited that segment's own damage zone. This is the port's equivalent. At render-tree build, each segment's draw object and its PrimaryDamageZone -- authored per segment in the skeleton stream, read by JMOVER.cpp:290 -- are recorded in MechRenderTree::segPick. The pick (BTL4VideoRenderer:: MechSegmentPick) ray-tests the per-segment bounding spheres on the live posed skeleton, using the draw-cached mLocalToWorld (at most one frame stale, fine for aiming). Selection is SPECIFICITY-FIRST: among the spheres the ray pierces, the smallest radius wins, normalized-distance tie-break. Both obvious rules were measured failing the same way before this one: the torso mesh's sphere (r~4.1 on the MadCat, vs shoulders at r~1.0) envelops nearly the whole mech, so its front face is nearest for any aim AND any near-body ray normalizes to ~0 against it. Limb spheres nest inside the envelope; smallest-pierced picks the most specific part on the aim line, and the torso wins only when no limb is threaded -- the per-part semantic the pod's mesh test produced. mech4.cpp tries the segment pick per candidate; the box PickRayHit survives only as the fallback (no tree yet, wrecked, spectator), still carrying zone -1 into the lottery, and a structure occlusion clears the zone. The winner's zone rides MECH_TARGET_SUBIDX + targetReticle.targetDamageZone into SendDamageMessage, so aimed hits now dispatch a real zone; the victim's handler applies it directly (bursts 2+ still re-lottery, authentic per the recovered @0x4a0230 loop). Bench, the same L/C/R sweep that exposed the bug: aiming left now lands 36/41 hits on zone 2 = jointlshoulder -- the left arm -- with a 0.30 thread score, where the same aim was a 6-way lottery spray before. The MadCat's authored segment->zone map is rich (shoulders 2/9, guns 6/17, hip 1, six leg zones, torso 0). Known approximations, flagged for field verification: sphere bounds rather than triangles, and a torso-envelope graze credits the torso where the pod's exact mesh test would have missed into air. The field protocol is the one the testers already ran on night 6: stationary mechs, short range, fire only at one arm -- the paper doll should now damage THAT arm. Diag: BT_PICK_LOG ([segpick] map at build, [pickwin] per pick). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e110b10ac8
commit
8a99972f22
@@ -537,6 +537,25 @@ HierarchicalDrawComponent*
|
||||
for (int op = 0; op < this_object->GetDrawOpCount(); ++op)
|
||||
this_object->GetDrawOp(op)->alphaTest = true;
|
||||
}
|
||||
|
||||
// #73: record the pickable segment geometry + its zone for the
|
||||
// aimed per-part pick (MechSegmentPick). The shadow proxy is not
|
||||
// a target; a rebuild resets the map with the tree.
|
||||
if (this_object != NULL && this_object->GetIsShadow() == 0)
|
||||
{
|
||||
MechRenderTree::SegPick sp;
|
||||
sp.obj = this_object;
|
||||
sp.zone = segment->GetPrimaryDamageZone(); // SEGMENT.h:107
|
||||
render_tree.segPick[segment->GetIndex()] = sp;
|
||||
if (getenv("BT_PICK_LOG"))
|
||||
DEBUG_STREAM << "[segpick] seg=" << segment->GetIndex()
|
||||
<< " '" << (const char *)segment->GetName()
|
||||
<< "' zone=" << sp.zone
|
||||
<< " r=" << this_object->mCullRadius
|
||||
<< " c=(" << this_object->mCullCenter.x << ","
|
||||
<< this_object->mCullCenter.y << ","
|
||||
<< this_object->mCullCenter.z << ")\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1315,6 +1334,118 @@ int BTWreckSinkTick(Entity *victim, float dt)
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// #73 -- the aimed PER-PART pick (see the header note). Ray-vs-sphere over
|
||||
// the per-segment draw objects recorded at tree build; world centers come
|
||||
// through the draw-cached mLocalToWorld (updated every drawn frame -- the
|
||||
// target being aimed at is on screen, so at most one frame stale).
|
||||
//
|
||||
int
|
||||
BTL4VideoRenderer::MechSegmentPick(
|
||||
Entity *mech,
|
||||
const float ray_start[3],
|
||||
const float ray_dir[3],
|
||||
float max_range,
|
||||
float hit_out[3],
|
||||
int *zone_out)
|
||||
{
|
||||
std::map<Entity*, MechRenderTree>::iterator it = mMechRenderTrees.find(mech);
|
||||
if (it == mMechRenderTrees.end() || it->second.wrecked)
|
||||
return 0;
|
||||
|
||||
// Selection is SPECIFICITY-FIRST: among the spheres the ray pierces, the
|
||||
// SMALLEST radius wins (normalized-distance tie-break). Neither nearest-
|
||||
// entry nor pure normalized distance works here, and both were measured
|
||||
// failing the same way: the torso mesh's sphere (r~4.1 on the MadCat)
|
||||
// envelops nearly the whole mech, so its front face is nearest for any aim
|
||||
// AND any near-body ray scores ~0 against it (d/r rewards giant spheres).
|
||||
// The limb spheres (shoulders r~1.0, guns r~2.2) nest INSIDE the torso
|
||||
// envelope; smallest-pierced picks the most specific part on the aim line,
|
||||
// and the torso wins only when no limb is threaded -- which is the per-part
|
||||
// semantic the 1995 mesh intersection produced.
|
||||
float bestR = 1e30f; // primary key: sphere radius (ascending)
|
||||
float bestScore = 1.0f; // tie-break: normalized perpendicular d2/r2
|
||||
float bestT = max_range;
|
||||
int bestZone = -1;
|
||||
int hitAny = 0;
|
||||
|
||||
std::map<int, MechRenderTree::SegPick>::iterator sp;
|
||||
for (sp = it->second.segPick.begin(); sp != it->second.segPick.end(); ++sp)
|
||||
{
|
||||
d3d_OBJECT *obj = sp->second.obj;
|
||||
if (obj == NULL || obj->mCullRadius <= 0.0f)
|
||||
continue;
|
||||
|
||||
D3DXMATRIX l2w = obj->GetLocalToWorld();
|
||||
D3DXVECTOR3 cw;
|
||||
D3DXVec3TransformCoord(&cw, &obj->mCullCenter, &l2w);
|
||||
|
||||
float ocx = cw.x - ray_start[0];
|
||||
float ocy = cw.y - ray_start[1];
|
||||
float ocz = cw.z - ray_start[2];
|
||||
float tca = ocx*ray_dir[0] + ocy*ray_dir[1] + ocz*ray_dir[2];
|
||||
float r = obj->mCullRadius;
|
||||
float oc2 = ocx*ocx + ocy*ocy + ocz*ocz;
|
||||
if (tca < 0.0f && oc2 > r*r)
|
||||
continue; // wholly behind the ray
|
||||
float d2 = oc2 - tca*tca;
|
||||
float r2 = r*r;
|
||||
if (d2 > r2)
|
||||
continue; // ray passes outside the sphere
|
||||
float thc = sqrtf(r2 - d2);
|
||||
float t = tca - thc;
|
||||
if (t < 0.0f)
|
||||
t = tca + thc; // ray starts inside: exit point
|
||||
if (t < 0.0f || t >= max_range)
|
||||
continue;
|
||||
|
||||
float score = d2 / r2; // 0 = dead-center thread
|
||||
if (r > bestR
|
||||
|| (r == bestR && score >= bestScore))
|
||||
continue;
|
||||
|
||||
bestR = r;
|
||||
bestScore = score;
|
||||
bestT = t;
|
||||
bestZone = sp->second.zone;
|
||||
hitAny = 1;
|
||||
}
|
||||
|
||||
if (!hitAny)
|
||||
return 0;
|
||||
|
||||
if (getenv("BT_PICK_LOG"))
|
||||
{
|
||||
static int s_pl = 0;
|
||||
if ((s_pl++ % 60) == 0)
|
||||
DEBUG_STREAM << "[pickwin] zone=" << bestZone
|
||||
<< " score=" << bestScore << " t=" << bestT << "\n" << std::flush;
|
||||
}
|
||||
|
||||
hit_out[0] = ray_start[0] + bestT * ray_dir[0];
|
||||
hit_out[1] = ray_start[1] + bestT * ray_dir[1];
|
||||
hit_out[2] = ray_start[2] + bestT * ray_dir[2];
|
||||
*zone_out = bestZone; // -1 = zone-less segment (lottery downstream)
|
||||
return 1;
|
||||
}
|
||||
|
||||
//
|
||||
// Game-side bridge (mech4.cpp's per-frame target pick; same access pattern as
|
||||
// the wreck swap below).
|
||||
//
|
||||
int BTMechSegmentPick(void *mech, const float ray_start[3], const float ray_dir[3],
|
||||
float max_range, float hit_out[3], int *zone_out)
|
||||
{
|
||||
if (mech == NULL || application == NULL)
|
||||
return 0;
|
||||
BTL4VideoRenderer *renderer =
|
||||
(BTL4VideoRenderer *)application->GetVideoRenderer();
|
||||
if (renderer == NULL)
|
||||
return 0;
|
||||
return renderer->MechSegmentPick((Entity *)mech, ray_start, ray_dir,
|
||||
max_range, hit_out, zone_out);
|
||||
}
|
||||
|
||||
//
|
||||
// Engine-side bridge (the ExplosionClassID dispatch calls this on effect 104).
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user