Files
BT412/game/reconstructed/dmgtable.hpp
T
arcattackandClaude Opus 4.8 d07ac7dd49 STEP 6 COMPLETE: cylinder hit-location LIVE — unaimed hits resolve to zones
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>
2026-07-08 08:32:17 -05:00

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