//===========================================================================//
// 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 <bt.hpp>
#pragma hdrstop

#include <math.h>

#if !defined(DMGTABLE_HPP)
#	include <dmgtable.hpp>
#endif
#if !defined(MECH_HPP)
#	include <mech.hpp>
#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
}
