DMGTABLE born (type-29 streams, BT411 byte-verified format): the height x angle grid -- DamageLookupTable rows by height, PieSlice cells by angle (rotate-with-torso rows add the live twist), DamageZonePercentTable = the cumulative hit-distribution roll (first threshold above the roll -- the BattleTech dice scatter). Loaded in the Mech ctor by the DamageZoneStream member's NAME (mech+0x444, resident); Mech::TakeDamageMessageHandler resolves invalidDamageZone hits through it (binary @0x4a0264 tail). The missile fuse becomes the authentic unaimed producer: zone -1 + the round's world position as the impact point (ENTITY3.HPP: "damage zones are only valid via reticle based weapons"). Fight-verified on the authentic 7-row bhk1 table: flat-flying SRMs strike low -> row 0 -> FEET AND LEG zones, side-correct by impact angle (+x right, -x left), identical geometry spread across the cell distribution by the roll. 17/17 missile lifecycle, zero faults; smoke + novice green. Also fixed: the ctor's reservedState zero-loop counted past the shrunken array. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
334 lines
7.8 KiB
C++
334 lines
7.8 KiB
C++
//===========================================================================//
|
|
// File: dmgtable.cpp //
|
|
// Project: BattleTech Brick: Mech damage //
|
|
// Contents: The cylinder hit-location table (unaimed damage -> zone) //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
|
// All Rights reserved worldwide //
|
|
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// RECONSTRUCTED from the binary via the BT411 reversal (byte-verified
|
|
// against the real type-29 streams, 18 tables). STREAM FORMAT:
|
|
//
|
|
// Table { i32 rowCount; Row[rowCount] }
|
|
// Row { i32 rotateWithTorso; i32 cellCount; Cell[cellCount] }
|
|
// Cell { i32 nameLen; char name[nameLen]; u8 0; i32 entryCount;
|
|
// Entry[entryCount] }
|
|
// Entry { f32 cumulativeThreshold; i32 zoneIndex }
|
|
//
|
|
// Real sample (ava1, 7 rows x 8 cells): rows 0-2 fixed to the chassis
|
|
// (feet/legs), rows 3-6 rotate with the torso; a foot cell =
|
|
// {0.5->16, 0.8->10, 1.0->5}.
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(DMGTABLE_HPP)
|
|
# include <dmgtable.hpp>
|
|
#endif
|
|
|
|
#if !defined(MECH_HPP)
|
|
# include <mech.hpp>
|
|
#endif
|
|
|
|
#if !defined(RANDOM_HPP)
|
|
# include <random.hpp>
|
|
#endif
|
|
|
|
#include <math.h>
|
|
|
|
static const Scalar kTwoPi = 6.2831855f; // _DAT_0049e810
|
|
static const Scalar kDegenerateEps = 1.0e-4f; // _DAT_0049ed04
|
|
|
|
//
|
|
// STAGED: the binary reads the mech's collision-cylinder height
|
|
// (mech+0x2ec -> +0xc); the collision volume isn't reconstructed yet, so a
|
|
// fixed standing height maps impacts onto the rows. (bhk1 torso mounts sit
|
|
// at y~4; 10u spans feet->cockpit plausibly.)
|
|
//
|
|
static const Scalar kStagedMechHeight = 10.0f;
|
|
|
|
//
|
|
//#############################################################################
|
|
// DamageZonePercentTable -- one angular cell (binary ctor @0049deb0; the
|
|
// entry fill @0049e5e4). The cell NAME string ([i32 len][chars][NUL]) is
|
|
// consumed and discarded, exactly as the binary does.
|
|
//#############################################################################
|
|
//
|
|
DamageZonePercentTable::DamageZonePercentTable(MemoryStream *stream)
|
|
{
|
|
Check(stream);
|
|
|
|
int name_length = 0;
|
|
*stream >> name_length;
|
|
if (name_length > 0)
|
|
{
|
|
stream->AdvancePointer(name_length);
|
|
}
|
|
stream->AdvancePointer(1); // the NUL
|
|
|
|
*stream >> entryCount;
|
|
thresholds = NULL;
|
|
zoneIndices = NULL;
|
|
if (entryCount > 0)
|
|
{
|
|
thresholds = new Scalar[entryCount];
|
|
Register_Pointer(thresholds);
|
|
zoneIndices = new int[entryCount];
|
|
Register_Pointer(zoneIndices);
|
|
for (int ee = 0; ee < entryCount; ++ee)
|
|
{
|
|
*stream >> thresholds[ee];
|
|
*stream >> zoneIndices[ee];
|
|
}
|
|
}
|
|
}
|
|
|
|
DamageZonePercentTable::~DamageZonePercentTable()
|
|
{
|
|
if (thresholds != NULL)
|
|
{
|
|
Unregister_Pointer(thresholds);
|
|
delete [] thresholds;
|
|
}
|
|
if (zoneIndices != NULL)
|
|
{
|
|
Unregister_Pointer(zoneIndices);
|
|
delete [] zoneIndices;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// SelectZone (binary @0049de14): uniform roll, first cumulative threshold
|
|
// ABOVE the roll wins (entries ascend); the last entry backstops.
|
|
//#############################################################################
|
|
//
|
|
int
|
|
DamageZonePercentTable::SelectZone()
|
|
{
|
|
Check(this);
|
|
|
|
if (entryCount <= 0)
|
|
{
|
|
return -1;
|
|
}
|
|
Scalar roll = (Scalar)Random;
|
|
for (int ee = 0; ee < entryCount; ++ee)
|
|
{
|
|
if (thresholds[ee] > roll)
|
|
{
|
|
return zoneIndices[ee];
|
|
}
|
|
}
|
|
return zoneIndices[entryCount - 1];
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// PieSlice -- one height row (binary ctor @0049e740).
|
|
//#############################################################################
|
|
//
|
|
PieSlice::PieSlice(Mech *mech, MemoryStream *stream)
|
|
{
|
|
Check(mech);
|
|
Check(stream);
|
|
|
|
owner = mech;
|
|
*stream >> rotateWithTorso;
|
|
*stream >> cellCount;
|
|
cells = NULL;
|
|
if (cellCount > 0)
|
|
{
|
|
cells = new DamageZonePercentTable *[cellCount];
|
|
Register_Pointer(cells);
|
|
for (int cc = 0; cc < cellCount; ++cc)
|
|
{
|
|
cells[cc] = new DamageZonePercentTable(stream);
|
|
Register_Object(cells[cc]);
|
|
}
|
|
}
|
|
}
|
|
|
|
PieSlice::~PieSlice()
|
|
{
|
|
if (cells != NULL)
|
|
{
|
|
for (int cc = 0; cc < cellCount; ++cc)
|
|
{
|
|
Unregister_Object(cells[cc]);
|
|
delete cells[cc];
|
|
}
|
|
Unregister_Pointer(cells);
|
|
delete [] cells;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// SelectSlice (binary @0049e678): rotate-with-torso rows add the LIVE torso
|
|
// twist to the impact angle (an upper-body ring follows the twist; legs
|
|
// stay chassis-fixed), wrap to [0,2pi), pick the cell by angular fraction.
|
|
//#############################################################################
|
|
//
|
|
DamageZonePercentTable*
|
|
PieSlice::SelectSlice(Scalar angle)
|
|
{
|
|
Check(this);
|
|
|
|
if (cellCount <= 0)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
Scalar a = angle;
|
|
if (rotateWithTorso)
|
|
{
|
|
a += owner->CurrentTorsoTwist();
|
|
while (a >= kTwoPi)
|
|
{
|
|
a -= kTwoPi;
|
|
}
|
|
while (a < 0.0f)
|
|
{
|
|
a += kTwoPi;
|
|
}
|
|
}
|
|
|
|
int index = (int)((Scalar)cellCount * a / kTwoPi);
|
|
if (index < 0)
|
|
{
|
|
index = 0;
|
|
}
|
|
if (index >= cellCount)
|
|
{
|
|
index = cellCount - 1;
|
|
}
|
|
return cells[index];
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// DamageLookupTable (binary ctor @0049ea48).
|
|
//#############################################################################
|
|
//
|
|
DamageLookupTable::DamageLookupTable(Mech *mech, MemoryStream *stream)
|
|
{
|
|
Check(mech);
|
|
Check(stream);
|
|
|
|
owner = mech;
|
|
*stream >> rowCount;
|
|
rows = NULL;
|
|
if (rowCount > 0)
|
|
{
|
|
rows = new PieSlice *[rowCount];
|
|
Register_Pointer(rows);
|
|
for (int rr = 0; rr < rowCount; ++rr)
|
|
{
|
|
rows[rr] = new PieSlice(mech, stream);
|
|
Register_Object(rows[rr]);
|
|
}
|
|
}
|
|
}
|
|
|
|
DamageLookupTable::~DamageLookupTable()
|
|
{
|
|
if (rows != NULL)
|
|
{
|
|
for (int rr = 0; rr < rowCount; ++rr)
|
|
{
|
|
Unregister_Object(rows[rr]);
|
|
delete rows[rr];
|
|
}
|
|
Unregister_Pointer(rows);
|
|
delete [] rows;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// ResolveHit (binary @0049eb54 + the glue @0049ed0c): world impact ->
|
|
// mech-local; clamp local height onto the cylinder -> row; impact angle =
|
|
// atan2(z, x) wrapped [0,2pi) (a degenerate on-axis hit rolls a random
|
|
// angle); row/angle -> cell -> the distribution roll -> a hull zone.
|
|
//#############################################################################
|
|
//
|
|
int
|
|
DamageLookupTable::ResolveHit(const Point3D &world_impact)
|
|
{
|
|
Check(this);
|
|
|
|
if (rowCount <= 0)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
//
|
|
// World -> mech-local.
|
|
//
|
|
LinearMatrix to_world;
|
|
to_world = owner->localToWorld;
|
|
LinearMatrix to_local;
|
|
to_local.Invert(to_world);
|
|
Point3D local;
|
|
local.Multiply(world_impact, to_local);
|
|
|
|
//
|
|
// Height -> row.
|
|
//
|
|
Scalar height = kStagedMechHeight;
|
|
Scalar y = local.y;
|
|
if (y < 0.0f)
|
|
{
|
|
y = 0.0f;
|
|
}
|
|
if (y > height)
|
|
{
|
|
y = height;
|
|
}
|
|
int row = (int)((Scalar)rowCount * y / height);
|
|
if (row < 0)
|
|
{
|
|
row = 0;
|
|
}
|
|
if (row >= rowCount)
|
|
{
|
|
row = rowCount - 1;
|
|
}
|
|
|
|
//
|
|
// Impact angle.
|
|
//
|
|
Scalar angle;
|
|
Scalar ax = (local.x < 0.0f) ? -local.x : local.x;
|
|
Scalar az = (local.z < 0.0f) ? -local.z : local.z;
|
|
if (ax > kDegenerateEps || az > kDegenerateEps)
|
|
{
|
|
angle = (Scalar)atan2((double)local.z, (double)local.x);
|
|
if (angle < 0.0f)
|
|
{
|
|
angle += kTwoPi;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
angle = (Scalar)Random * kTwoPi;
|
|
}
|
|
|
|
DamageZonePercentTable *cell = rows[row]->SelectSlice(angle);
|
|
int zone = (cell != NULL) ? cell->SelectZone() : -1;
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[cyl] impact local=(" << local.x
|
|
<< "," << local.y << "," << local.z
|
|
<< ") row=" << row << "/" << rowCount
|
|
<< " angle=" << angle
|
|
<< " -> zone " << zone << endl << flush;
|
|
}
|
|
return zone;
|
|
}
|