#92: map the direct-fire hit->zone path; the FOOT is pickable over only 3% of a mech's height
Oracle, night 7: "Unable to damage the foot panels on Loki via direct fire from
front or side. Was able to damage on Thor."
FIRST, THE MECHANISM -- it is not what the issue assumed (missing collision
geometry). A direct-fire hit gets its zone one of two ways, decided at
Mech::TakeDamageMessageHandler by `invalidDamageZone`:
AIMED (invalidDamageZone=0): the zone rides in on the message from the
per-part aim pick, MechSegmentPick (#73, btl4vid.cpp).
UNAIMED (invalidDamageZone=1): DamageLookupTable::ResolveHit runs the authored
cylinder lottery -- impact HEIGHT picks a layer, ANGLE picks a pie
slice, and a weighted random roll picks the zone from that slice.
Measured live, both paths are in use: the Thor logged 16 unaimed hits
(invalidZone=1 zone=-1 -> ResolveHit) alongside aimed ones carrying real zones.
THE FINDING. MechSegmentPick is a BOUNDING-SPHERE test whose primary key is
SMALLEST RADIUS WINS -- a larger sphere can never beat a smaller one the ray also
grazes. The leg spheres, dumped in world space and IDENTICAL on both chassis:
knee centre y=1.868 r=1.505 spans y 0.364 .. 3.373
toe centre y=0.302 r=1.689 spans y -1.387 .. 1.991
The toe sphere is BIGGER than the knee's and they overlap heavily, so the toe can
only win where the ray misses the knee sphere outright -- i.e. below y=0.364.
Against a reference height of 11.16 that is a 0.36-unit window, **3.3% of the
mech's height**, and it sits right on the ground. Everywhere else a shot at the
foot is credited to the LEG. That is the reported symptom.
This is a PORT ARTIFACT, not authentic: btl4vid.cpp's own comment concedes the
sphere test approximates "the per-part semantic the 1995 mesh intersection
produced". Real mesh intersection has no such interference -- aiming at the foot
mesh hits the foot.
NOT EXPLAINED, and stated plainly: the per-CHASSIS asymmetry. Loki and Thor have
identical leg spheres, identical foot geometry (LOK_LFOT.BGF and THR_LFOT.BGF are
both 3082 bytes with the same token layout), and identical foot layers in their
damage tables (only the upper/cockpit layers differ). So nothing found here says
the Loki should behave differently from the Thor. The bench could not settle it
because BT_AIM moves the drawn RETICLE, not the pick ray -- there is currently no
harness to aim the pick at a chosen height. That harness is the next step.
Diagnostics added:
[pickgeom] one-shot dump of every pick sphere in WORLD space (zone, r, centre)
[pickcand] which spheres a ray actually threaded, their perpendicular d, and
which won -- the probe that makes "smallest wins" visible
[dmgtable] the whole authored DamageLookupTable: layers, slices, zone weights
[dmgresolve] per hit: localY, heightRef, layer, theta, resolved zone
[cylgate] invalidDamageZone / table pointer / incoming zone at the gate
(the last three under BT_DMGTABLE_LOG, the first two under BT_PICK_LOG)
plus scratchpad/night8/footpick.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8daac37e40
commit
5a425320b7
@@ -1621,6 +1621,58 @@ int
|
||||
hitAny = 1;
|
||||
}
|
||||
|
||||
// #92 probe: which spheres did the ray actually THREAD, and which won?
|
||||
// "smallest radius wins" means a big sphere can never beat a small one that
|
||||
// the ray also grazes -- so a foot can be unhittable behind its own knee.
|
||||
if (getenv("BT_PICK_LOG"))
|
||||
{
|
||||
static int s_pc = 0;
|
||||
if ((s_pc++ % 60) == 0)
|
||||
{
|
||||
// one-shot: EVERY sphere in world space, so the foot/knee geometry
|
||||
// can be worked out analytically rather than by aim-hunting
|
||||
static int s_dumped = 0;
|
||||
if (!s_dumped)
|
||||
{
|
||||
s_dumped = 1;
|
||||
DEBUG_STREAM << "[pickgeom] ray_start=(" << ray_start[0] << ","
|
||||
<< ray_start[1] << "," << ray_start[2] << ")" << std::endl;
|
||||
std::map<int, MechRenderTree::SegPick>::iterator gp;
|
||||
for (gp = it->second.segPick.begin(); gp != it->second.segPick.end(); ++gp)
|
||||
{
|
||||
d3d_OBJECT *o3 = gp->second.obj;
|
||||
if (o3 == NULL || o3->mCullRadius <= 0.0f) continue;
|
||||
D3DXMATRIX m3 = o3->GetLocalToWorld();
|
||||
D3DXVECTOR3 c3;
|
||||
D3DXVec3TransformCoord(&c3, &o3->mCullCenter, &m3);
|
||||
DEBUG_STREAM << "[pickgeom] zone=" << gp->second.zone
|
||||
<< " r=" << o3->mCullRadius
|
||||
<< " world=(" << c3.x << "," << c3.y << "," << c3.z << ")"
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
DEBUG_STREAM << "[pickcand]";
|
||||
for (sp = it->second.segPick.begin(); sp != it->second.segPick.end(); ++sp)
|
||||
{
|
||||
d3d_OBJECT *o2 = sp->second.obj;
|
||||
if (o2 == NULL || o2->mCullRadius <= 0.0f) continue;
|
||||
D3DXMATRIX m2 = o2->GetLocalToWorld();
|
||||
D3DXVECTOR3 c2;
|
||||
D3DXVec3TransformCoord(&c2, &o2->mCullCenter, &m2);
|
||||
float ax = c2.x - ray_start[0], ay = c2.y - ray_start[1], az = c2.z - ray_start[2];
|
||||
float tc = ax*ray_dir[0] + ay*ray_dir[1] + az*ray_dir[2];
|
||||
float rr = o2->mCullRadius;
|
||||
float a2 = ax*ax + ay*ay + az*az;
|
||||
if (tc < 0.0f && a2 > rr*rr) continue;
|
||||
float dd = a2 - tc*tc;
|
||||
if (dd > rr*rr) continue; // not threaded
|
||||
DEBUG_STREAM << " {z" << sp->second.zone << " r=" << rr
|
||||
<< " d=" << sqrtf(dd > 0.0f ? dd : 0.0f) << "}";
|
||||
}
|
||||
DEBUG_STREAM << " -> WON z" << bestZone << " r=" << bestR << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hitAny)
|
||||
return 0;
|
||||
|
||||
|
||||
@@ -211,6 +211,44 @@ DamageLookupTable::DamageLookupTable(
|
||||
{
|
||||
layers.push_back(new PieSlice(owner, stream));
|
||||
}
|
||||
|
||||
// #92 -- "unable to damage the foot panels on Loki ... could on Thor".
|
||||
// Direct-fire damage does NOT use the mesh or the aim pick: the impact's
|
||||
// HEIGHT picks a layer, its ANGLE picks a slice, and a weighted random roll
|
||||
// picks the zone from that slice. So a zone that appears in NO slice of the
|
||||
// lowest layer is simply unreachable by direct fire. Dump the whole table.
|
||||
if (getenv("BT_DMGTABLE_LOG"))
|
||||
DumpTable();
|
||||
}
|
||||
|
||||
//
|
||||
// #92 diagnostic: print every layer / slice / weighted zone entry.
|
||||
//
|
||||
void DamageLookupTable::DumpTable() const
|
||||
{
|
||||
DEBUG_STREAM << "[dmgtable] layers=" << layerCount << std::endl;
|
||||
for (int L = 0; L < (int)layers.size(); ++L)
|
||||
{
|
||||
const PieSlice *layer = layers[L];
|
||||
DEBUG_STREAM << "[dmgtable] layer " << L
|
||||
<< " rotateWithTorso=" << layer->DiagRotateWithTorso()
|
||||
<< " slices=" << layer->DiagSliceCount() << std::endl;
|
||||
for (int S = 0; S < layer->DiagSliceCount(); ++S)
|
||||
{
|
||||
const DamageZonePercentTable *leaf = layer->DiagSlice(S);
|
||||
if (leaf == 0) continue;
|
||||
DEBUG_STREAM << "[dmgtable] slice " << S << ":";
|
||||
Scalar prev = 0.0f;
|
||||
for (int E = 0; E < leaf->DiagEntryCount(); ++E)
|
||||
{
|
||||
Scalar cum = leaf->DiagCumulative(E);
|
||||
DEBUG_STREAM << " z" << leaf->DiagZone(E)
|
||||
<< "=" << (int)((cum - prev) * 100.0f + 0.5f) << "%";
|
||||
prev = cum;
|
||||
}
|
||||
DEBUG_STREAM << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DamageLookupTable::~DamageLookupTable() // @0x49eadc
|
||||
@@ -259,5 +297,20 @@ int
|
||||
}
|
||||
|
||||
DamageZonePercentTable *leaf = layer->SelectSlice(theta); // FUN_0049e678
|
||||
return leaf ? leaf->SelectZone() : -1; // FUN_0049de14
|
||||
int resolved = leaf ? leaf->SelectZone() : -1; // FUN_0049de14
|
||||
|
||||
// #92: which LAYER does a given impact height land in? A foot shot that
|
||||
// lands in a layer above the foot layer can never roll a foot zone.
|
||||
if (getenv("BT_DMGTABLE_LOG"))
|
||||
{
|
||||
static int s_rh = 0;
|
||||
if (s_rh++ < 400)
|
||||
DEBUG_STREAM << "[dmgresolve] localY=" << local.y
|
||||
<< " heightRef=" << heightRef
|
||||
<< " frac=" << (depth / heightRef)
|
||||
<< " layer=" << layerIndex << "/" << layerCount
|
||||
<< " theta=" << theta
|
||||
<< " -> zone " << resolved << std::endl;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -105,6 +105,11 @@ class Point3D;
|
||||
// content-build authoring path -- is not part of the runtime port; the
|
||||
// shipped .RES is pre-built. Omitted here.)
|
||||
|
||||
// #92 diagnostics (read-only)
|
||||
int DiagEntryCount() const { return (int)entries.size(); }
|
||||
Scalar DiagCumulative(int i) const { return entries[i].cumulative; }
|
||||
int DiagZone(int i) const { return entries[i].zoneIndex; }
|
||||
|
||||
protected:
|
||||
Mech *owner; // @0x0c
|
||||
std::vector<Entry> entries; // @0x14 (was ReconTable) -- sorted ascending
|
||||
@@ -138,6 +143,11 @@ class Point3D;
|
||||
//
|
||||
DamageZonePercentTable *SelectSlice(Scalar theta) const;
|
||||
|
||||
// #92 diagnostics (read-only)
|
||||
int DiagSliceCount() const { return (int)slices.size(); }
|
||||
const DamageZonePercentTable *DiagSlice(int i) const { return slices[i]; }
|
||||
int DiagRotateWithTorso() const { return rotateWithTorso; }
|
||||
|
||||
protected:
|
||||
Mech *owner; // @0x0c
|
||||
std::vector<DamageZonePercentTable*>
|
||||
@@ -172,6 +182,7 @@ class Point3D;
|
||||
// the table is empty or the roll falls through (treated as a miss).
|
||||
//
|
||||
int ResolveHit(const Point3D &impact) const;
|
||||
void DumpTable() const; // #92 diagnostic
|
||||
|
||||
int LayerCount() const { return layerCount; } // bring-up verify
|
||||
|
||||
|
||||
@@ -949,6 +949,17 @@ void
|
||||
}
|
||||
|
||||
DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!)
|
||||
// #92: when this gate fails the zone keeps whatever the message carried --
|
||||
// which for an unaimed weapon hit is 0, i.e. EVERY shot lands on zone 0.
|
||||
if (getenv("BT_DMGTABLE_LOG"))
|
||||
{
|
||||
static int s_g = 0;
|
||||
if (s_g++ < 40)
|
||||
DEBUG_STREAM << "[cylgate] invalidZone=" << (int)message->invalidDamageZone
|
||||
<< " table=" << (void *)table
|
||||
<< " incomingZone=" << message->damageZone
|
||||
<< " type=" << (int)message->damageData.damageType << std::endl;
|
||||
}
|
||||
if (message->invalidDamageZone && table != 0)
|
||||
{
|
||||
int zone = table->ResolveHit(message->damageData.impactPoint);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# #92: "Unable to damage the foot panels on Loki via direct fire ... was able to
|
||||
# damage on Thor" (Oracle). The aimed per-part pick (MechSegmentPick, #73) is a
|
||||
# BOUNDING-SPHERE test whose primary key is SMALLEST RADIUS WINS -- a larger
|
||||
# sphere can never beat a smaller one, whatever the aim. So if a chassis's foot
|
||||
# sphere is bigger than its lower-leg sphere, every ray that threads both is
|
||||
# credited to the leg and the foot is unhittable. Dump the spheres per chassis.
|
||||
set -x
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
V="$1"
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=$V/" MP.EGG > FOOT.EGG
|
||||
LOG=footpick_$V.log
|
||||
rm -f "$LOG"
|
||||
export BT_PICK_LOG=1 BT_DMGTABLE_LOG=1
|
||||
export BT_START_INSIDE=0 # external: the whole mech tree is built + shown
|
||||
export BT_SPAWN_ENEMY=1 # target of the SAME chassis (uses the mission model)
|
||||
export BT_GOTO=enemy BT_GOTO_STOP=25 # close in so the foot subtends a real angle
|
||||
export BT_AIM="${2:-0 -0.35}" # aim LOW -- at the feet
|
||||
export BT_AUTOFIRE=1 BT_DMG_LOG=1 # shoot it and log the resolved zone
|
||||
bt_launch "$LOG" FOOT.EGG 0x03
|
||||
sleep 70
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
Reference in New Issue
Block a user