Files
BT411/game/reconstructed/dmgtable.hpp
T
Joe DiPrimaandClaude Opus 5 5a425320b7 #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>
2026-08-01 19:47:30 -05:00

197 lines
7.9 KiB
C++

//===========================================================================//
// File: dmgtable.hpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Damage lookup table -- layered / pie-slice hit-location selector //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
// 07/08/26 RE Completed for RUNTIME: real vector storage (was no-op //
// ReconTable shims), stream name-string read, PieSlice flag //
// fix, ResolveHit->zone chain, named accessors. Byte-format //
// verified against BTL4.RES type-29 (see combat-damage.md). //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary. Behaviour follows the Ghidra
// pseudo-C for the cluster @0x49de14..@0x49f5fb. The three-level resolver is:
//
// DamageLookupTable -- stack of armour LAYERS by penetration depth (height)
// -> PieSlice -- an angular sector wheel (optionally torso-relative)
// -> DamageZonePercentTable
// -- a weighted list of {cumulative %, zone segment index}
// whose percentages rise to 1.0; a uniform random roll
// then selects the final damage-zone index.
//
// Stream format (type-29 DamageLookupTableStream), byte-verified against the
// shipped BTL4.RES (18 tables, EXACT consumption):
// Table { i32 layerCount; Layer[layerCount] }
// Layer { i32 rotateWithTorso; i32 sliceCount; Slice[sliceCount] }
// Slice { i32 nameLen; char name[nameLen]; u8 0x00; i32 count; Entry[count] }
// Entry { f32 cumulativeThreshold; i32 zoneIndex }
//
// Runtime lookup (FUN_0049eb54 + FUN_0049e678 + FUN_0049de14 + glue FUN_0049ed0c):
// local = owner->WorldToLocal(impact) (owner+0xd0 xform)
// layer = floor(layerCount * local.y / heightRef) (heightRef=owner+0x2ec[+0xc])
// theta = atan2(local.z, local.x) [+ torso twist] (degenerate -> random*2PI)
// slice = floor(sliceCount * theta / 2PI)
// zone = first entry whose cumulative threshold > RandomUnit()
//
// vtables: 0x50bd84 (DamageLookupTable), 0x50bd90 (PieSlice),
// 0x50bd9c (DamageZonePercentTable / leaf).
//
#if !defined(DMGTABLE_HPP)
# define DMGTABLE_HPP
#include <vector>
#if !defined(PLUG_HPP)
# include <plug.hpp>
#endif
#include "mechrecon.hpp" // reconstruction shim (Scalar, DebugStream, ...)
//##################### Forward Class Declarations #######################
class Mech;
class NotationFile;
class MemoryStream;
class Point3D;
//###########################################################################
//##################### DamageZonePercentTable ##########################
//###########################################################################
//
// Leaf node. Holds a sorted, cumulative-weighted list of damage-zone segment
// indices; a uniform random roll turns a [0,1) draw into one of those zones.
// (vtable @0x50bd9c, ctor @0x49deb0, dtor @0x49df80.)
//
class DamageZonePercentTable:
public Plug
{
public:
struct Entry // binary: zoneIndex@+0xc, cumulative@+0x14
{
Scalar cumulative; // ascending sort key (rises to 1.0)
int zoneIndex; // damageZones[] / skeleton segment index
};
DamageZonePercentTable() {} // stack-temp / offline builder
DamageZonePercentTable( // @0x49deb0 (ReadEntries @0x49e5e4)
Mech *owner,
MemoryStream *stream
);
~DamageZonePercentTable(); // @0x49df80
Logical TestInstance() const { return True; } // @0x49e000
//
// @0x49de14 -- uniform weighted roll: draw a [0,1) sample (RandomUnit,
// FUN_00408050) and return the zone index of the first entry whose
// cumulative threshold exceeds the draw; -1 if the list is empty.
//
int SelectZone() const;
//
// @0x49e5e4 -- read the leading cell NAME string (i32 len + len+1 bytes
// incl. NUL, per FUN_00402948), then the entry count and that many
// {cumulative, zoneIndex} pairs.
//
void ReadEntries(MemoryStream *stream);
// (offline @0x49e00c BuildFromNotation / @0x49e524 WriteEntries -- the
// 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
};
//###########################################################################
//############################# PieSlice ################################
//###########################################################################
//
// An angular sector "wheel". N equal-angle slices (each 2*PI/N rad); the slice
// containing the incoming hit direction is chosen and its leaf table rolled.
// When rotateWithTorso is set the incoming angle is measured relative to the
// live torso twist (owner->TorsoHeading, == torso+0x1d8).
// (vtable @0x50bd90, ctor @0x49e740.)
//
class PieSlice:
public Plug
{
public:
PieSlice() : owner(0), rotateWithTorso(0), sliceCount(0) {}
PieSlice( // @0x49e740
Mech *owner,
MemoryStream *stream
);
~PieSlice();
//
// @0x49e678 -- pick the slice for incoming angle 'theta' (radians). When
// rotateWithTorso, theta is shifted by the live torso twist and wrapped
// into [0, 2*PI); slice index = floor(theta * sliceCount / 2*PI).
//
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*>
slices; // @0x14 (was ReconTable)
int rotateWithTorso; // @0x0c-flag in binary (this[3])
int sliceCount; // @0x2c (this[0xb])
};
//###########################################################################
//######################### DamageLookupTable ###########################
//###########################################################################
//
// Top-level table streamed from the mech's ".tbl". A stack of armour LAYERS:
// the higher the local-frame impact, the higher the layer index. Each layer
// is a PieSlice wheel. (vtable @0x50bd84, ctor @0x49ea48, dtor @0x49eadc.)
//
class DamageLookupTable:
public Plug
{
public:
DamageLookupTable( // @0x49ea48
Mech *owner,
MemoryStream *stream
);
~DamageLookupTable(); // @0x49eadc
Logical TestInstance() const { return True; } // @0x49eb48
//
// @0x49eb54 (+ @0x49e678 slice + @0x49de14 roll, chained by glue @0x49ed0c)
// -- resolve a world impact point to a damage-zone index. Returns -1 when
// 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
protected:
Mech *owner; // @0x0c
std::vector<PieSlice*>
layers; // @0x10 (was ReconTable)
int layerCount; // @0x28 (this[10])
};
#endif