The Mech per-impact hit-location resolver (the cylinder damage table) is now functional, wired, and runtime-verified [T2]. Unaimed (zone==-1) hits — the collision-damage path — now resolve an impact point to a damage zone via the authentic height x angle grid + weighted dice roll, instead of dropping. dmgtable.cpp/.hpp was a non-functional skeleton on no-op ReconTable/stream shims; backed it with real std::vector storage and fixed 5 latent runtime bugs: - ReadEntries now consumes the leading cell name-string ([i32 len][len+1]) - PieSlice ctor reads rotateWithTorso into the correct member - SelectSlice direct-indexes (was int lookup on a float-keyed table) - ResolveHit returns the zone (chains SelectSlice -> SelectZone) - real MemoryStream::ReadBytes (was a variadic no-op) mech.cpp ctor: replaced the empty-name StandingAnimation stub with the real load — FindResourceDescription(dzRes->resourceName, type 0x1d) -> stream -> new DamageLookupTable, cached at mech[0x111]; ~Mech deletes it. Mech::TakeDamageMessageHandler override registered (MESSAGE_ENTRY overlays Entity's by ID): on invalidDamageZone, resolve via the table then base-route; aimed reticle hits pass through unchanged. Three named accessors (no databinding-trap raw reads): WorldToLocal (localToWorld.MultiplyByInverse), CylinderReferenceHeight (standingTemplateMaxY == collisionTemplate->maxY == binary mech+0x2ec[+0xc]), TorsoHeading via a BTGetTorsoTwist bridge in torso.cpp (Torso::CurrentTwist == torso+0x1d8; torso.hpp cannot be included into mech.cpp — subsystem-stub collision). Stream format + geometry + roll + handler were all byte-verified against the shipped BTL4.RES type-29 resources (18 tables, exact consumption) and the disassembly (FUN_0049eb54/e678/de14, glue 0x49ed0c, handler @0x4a037a). Runtime: boots clean, "[cyl] table 'bhk1' layers=7" (exact byte-verified layer count, found by name), mech spawns + walks, no asserts/AV/0xCDCDCDCD. Env gate BT_CYL_LOG=1. Unblocks collision-damage application. KB updated (combat-damage.md STEP 6 COMPLETE, open-questions.md marked done). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
186 lines
7.4 KiB
C++
186 lines
7.4 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.)
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
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
|