//===========================================================================// // File: dmgtable.cpp // // 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 (was a non-functional skeleton on // // no-op ReconTable/stream shims). See dmgtable.hpp header. // //---------------------------------------------------------------------------// // 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, cross-checked BYTE-FOR-BYTE // against the shipped BTL4.RES type-29 resources (18 tables, exact stream // consumption -- see context/combat-damage.md). Each method cites its @ADDR. // // Float constants (read-only globals in the decomp): // _DAT_0049e520 = 1.0f (percentage-sum target, offline validation) // _DAT_0049e72c / _DAT_0049e810 / _DAT_0049ed08 = 6.2831853f = 2*PI // _DAT_0049ed04 = ~1e-4f (degenerate-direction epsilon) // _DAT_0049ed00 / _DAT_0049e730 = 0.0f // // Engine-helper name mapping: // FUN_00408050 RandomUnit() -> Scalar in [0,1) (engine RandomGenerator) // FUN_00402948 read shared string [i32 len][len+1 bytes: chars + NUL] // FUN_004dc8ec atan2f() FUN_004dcd00 fabsf() FUN_004dcd10/94 floorf // FUN_00408bf8 world->local point xform (owner+0xd0) == Mech::WorldToLocal // #include #pragma hdrstop #include #if !defined(DMGTABLE_HPP) # include #endif #if !defined(MECH_HPP) # include #endif // // Tuning constants observed as read-only globals in the decomp. // static const Scalar PercentSumTarget = 1.0f; // _DAT_0049e520 static const Scalar TwoPi = 6.2831853f; // _DAT_0049e72c / _DAT_0049e810 static const Scalar DirEpsilon = 1.0e-4f; // _DAT_0049ed04 //########################################################################### // DamageZonePercentTable (leaf) //########################################################################### // // @0x49deb0 -- stream constructor. Stores the owner and rebuilds the entry // list from the object stream (ReadEntries @0x49e5e4). // DamageZonePercentTable::DamageZonePercentTable( Mech *owner, MemoryStream *stream ): Plug(), owner(owner) { ReadEntries(stream); } DamageZonePercentTable::~DamageZonePercentTable() // @0x49df80 { // entries is a value vector -- frees itself. } // // @0x49de14 -- the weighted random roll. Walks the entries in ascending // cumulative order, draws ONE uniform sample in [0,1) and returns the zone // index of the first entry whose cumulative threshold exceeds the draw. // -1 when the table is empty (treated as a miss by the caller). // int DamageZonePercentTable::SelectZone() const { Scalar roll = RandomUnit(); // FUN_00408050 for (unsigned i = 0; i < entries.size(); ++i) { if (roll < entries[i].cumulative) // first threshold past the draw { return entries[i].zoneIndex; } } return -1; } // // @0x49e5e4 -- read the leading cell NAME string (FUN_00402948: [i32 len] then // len+1 bytes = chars + trailing NUL), then the entry count and that many // {cumulativePercent, zoneIndex} pairs. The name is descriptive only (the // binary caches it but the roll never uses it), so it is consumed and dropped. // void DamageZonePercentTable::ReadEntries(MemoryStream *stream) { int nameLen = 0; stream->ReadBytes(&nameLen, 4); // FUN_00402948: string length if (nameLen > 0) { char scratch[128]; int total = nameLen + 1; // chars + NUL (FUN_00402948 reads len+1) if (total > (int)sizeof(scratch)) total = (int)sizeof(scratch); stream->ReadBytes(scratch, total); // consume name + NUL } int count = 0; stream->ReadBytes(&count, 4); entries.reserve(count > 0 ? count : 0); for (int i = 0; i < count; ++i) { Entry e; stream->ReadBytes(&e.cumulative, 4); // f32 cumulative threshold stream->ReadBytes(&e.zoneIndex, 4); // i32 zone segment index entries.push_back(e); } } //########################################################################### // PieSlice //########################################################################### // // @0x49e740 -- stream constructor (vtable 0x50bd90). Reads the RotateWithTorso // flag (binary this[3]) and the slice count (this[0xb]) from the stream, then // builds 'sliceCount' leaf tables. (The torso orientation is read live via // owner->TorsoHeading() in SelectSlice -- no cached raw pointer.) // PieSlice::PieSlice( Mech *owner, MemoryStream *stream ): Plug(), owner(owner), rotateWithTorso(0), sliceCount(0) { stream->ReadBytes(&rotateWithTorso, 4); // binary reads into this[3] stream->ReadBytes(&sliceCount, 4); // this[0xb] slices.reserve(sliceCount > 0 ? sliceCount : 0); for (int i = 0; i < sliceCount; ++i) // binary loops i=1..count building leaves { slices.push_back(new DamageZonePercentTable(owner, stream)); } } PieSlice::~PieSlice() { for (unsigned i = 0; i < slices.size(); ++i) { delete slices[i]; } } // // @0x49e678 -- choose the slice for incoming angle 'theta'. When the wheel // rotates with the torso, add the live torso twist (torso+0x1d8, via // owner->TorsoHeading) and wrap into [0, 2*PI). Slice index = // floor(theta * sliceCount / 2*PI) -- direct 0-based index (equivalent to the // binary's float-key Find((index+1)*2*PI/sliceCount)). // DamageZonePercentTable * PieSlice::SelectSlice(Scalar theta) const { if (sliceCount <= 0) return 0; if (rotateWithTorso) // binary this[3] != 0 { theta += owner->TorsoHeading(); // torso+0x1d8 if (theta >= TwoPi) theta -= TwoPi; if (theta < 0.0f) theta += TwoPi; } int index = (int)floorf(theta * (Scalar)sliceCount * (1.0f / TwoPi)); if (index < 0) index = 0; if (index > sliceCount - 1) index = sliceCount - 1; return slices[index]; } //########################################################################### // DamageLookupTable (top -- layered) //########################################################################### // // @0x49ea48 -- stream constructor (vtable 0x50bd84). Reads the layer count and // builds that many PieSlice layers from the stream. // DamageLookupTable::DamageLookupTable( Mech *owner, MemoryStream *stream ): Plug(), owner(owner), layerCount(0) { stream->ReadBytes(&layerCount, 4); // this[10] layers.reserve(layerCount > 0 ? layerCount : 0); for (int i = 0; i < layerCount; ++i) { layers.push_back(new PieSlice(owner, stream)); } } DamageLookupTable::~DamageLookupTable() // @0x49eadc { for (unsigned i = 0; i < layers.size(); ++i) { delete layers[i]; } } // // @0x49eb54 (+ slice @0x49e678 + roll @0x49de14, chained by glue @0x49ed0c) -- // resolve a world impact point to a damage-zone index. // local = owner->WorldToLocal(impact) (owner+0xd0 xform) // layer = clamp(floor(layerCount * local.y / heightRef), 0, layerCount-1) // theta = atan2(local.z, local.x) (degenerate direction -> random*2*PI) // zone = layer.SelectSlice(theta).SelectZone() // Returns -1 when the table is empty or the roll falls through (a miss). // int DamageLookupTable::ResolveHit(const Point3D &impact) const { if (layerCount <= 0) return -1; Point3D local = owner->WorldToLocal(impact); // FUN_00408bf8 Scalar heightRef = owner->CylinderReferenceHeight(); // owner+0x2ec[+0xc] if (heightRef <= 0.0f) return -1; // --- height -> layer (penetration depth) --- Scalar depth = local.y; // binary local_2c int layerIndex = (int)floorf((Scalar)layerCount * (depth / heightRef)); if (layerIndex < 0) layerIndex = 0; if (layerIndex > layerCount - 1) layerIndex = layerCount - 1; PieSlice *layer = layers[layerIndex]; // --- in-plane hit direction -> incident angle (else random scatter) --- Scalar theta; if (fabsf(local.z) > DirEpsilon && fabsf(local.x) > DirEpsilon) { theta = atan2f(local.z, local.x); // FUN_004dc8ec if (theta < 0.0f) theta += TwoPi; } else { theta = RandomUnit() * TwoPi; // degenerate -> uniform angle } DamageZonePercentTable *leaf = layer->SelectSlice(theta); // FUN_0049e678 return leaf ? leaf->SelectZone() : -1; // FUN_0049de14 }