//==========================================================================//
// File:        mechdmg.cpp                                                  //
// Project: BattleTech                                                       //
// Contents: Mech::DamageZone -- maps incoming damage to mech locations /    //
//           subsystems (the dz_* zones streamed from the .BGF SV_SPECIAL    //
//           tokens), critical-hit selection, LOD redirection, and the       //
//           descend-on-destruction segment walk.                            //
//---------------------------------------------------------------------------//
//   Date    Who  Modification                                               //
// --------  ---  ---------------------------------------------------------- //
// 06/05/95  JM  Initial coding.                                             //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc.                     //
// All Rights reserved worldwide                                             //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL               //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, recovered/all/
// part_012.c).  Class shape is the SURVIVING MECHDMG.HPP; bodies follow the
// decompiled object at the cited @ADDRs.  The companion Mech-side loop that
// streams every zone, Mech::CreateDamageZoneStream @004a474c, lives in
// mech.cpp; this file is the per-zone class.
//
// Float constants recovered from the code section (section_dump.txt):
//      _DAT_0049c99c = 1.0f   _DAT_0049c9a0 = 0.5f    _DAT_0049c9a4 = 0.0f
//      _DAT_0049cacc = 0.0f   _DAT_0049cad0 = 1.0f
//      _DAT_0049ce48 = 0.5f   _DAT_0049ce4c = 1.0f
//      _DAT_0049c50c = 0.25f  _DAT_0049c510 = 10.0f   (LOD reuse-window seconds)
//      _DAT_0049c5f8 = 0.0f   _DAT_0049c5fc = 1.0f    (structure clamp)
//      _DAT_0049d1c4 = 1.0f   _DAT_0049d1c8 = 1.0e-4f _DAT_0049d1cc = 0.5f (leg halving)
//
// Helper-function name mapping (engine internals referenced by the decomp):
//      FUN_0041df5c  DamageZone::DamageZone (base ctor, vtable @004e3cf0)
//      FUN_0041e394  DamageZone::~DamageZone (base dtor)
//      FUN_0041e4e0  DamageZone::TakeDamage  (base armor/structure model)
//      FUN_0041e590  DamageZone::CreateStreamedDamageZone (base streamer)
//      FUN_00424767  DamageZoneIndexTable::TableOf(0,1)
//      FUN_0042478a  DamageZoneIndexTable::~TableOf
//      FUN_00424830  DamageZoneIndexTableIterator::TableIteratorOf(&table)
//      FUN_0042484f  DamageZoneIndexTableIterator::~TableIteratorOf
//      FUN_00424716  IntegerPlug::PlugOf(&int)
//      FUN_00424811  ...IteratorOf(&list)   (segment child iterators)
//      FUN_00417ab4  SlotOf<>::Resolve() / SharedData::Resolve()
//      FUN_00417858  SlotOf<>::Release
//      FUN_0041a1a4  IsDerivedFrom(classDerivations)
//      FUN_0041b9ec  AlarmIndicator(levels)         FUN_0041bbd8  AlarmIndicator::SetLevel(n)
//      FUN_00420ea4  EntityID::operator=(copy)      FUN_00420ef4  EntityID::operator=
//      FUN_0042104c  EntityID::operator==
//      FUN_00414b60  TheTime.CurrentTick()  (/ DAT_0052140c == TicksPerSecond)
//      FUN_00408050  (Scalar)Random -> [0,1)      FUN_0040807c(rng,n) Random(n)
//      FUN_004ac07c  Subsystem::ApplyDamage(damage) -> Scalar
//      FUN_004b11bc  Generator::ForceShortRecovery (class derivations @0050f4bc)
//      FUN_0049fb54  Mech::<query torso/leg destroyed state>
//      FUN_0040485c/00404720/00404088/00404190/004040d8  NotationFile readers
//      FUN_00402298/004022b0/004022d0  operator new / array new / delete
//      FUN_004dbb24/004db92c/004d9c38/004dbd4c  DebugStream << / endl
//
#include <bt.hpp>
#pragma hdrstop

#if !defined(MECHDMG_HPP)
#	include <mechdmg.hpp>
#endif
#if !defined(MECH_HPP)
#	include <mech.hpp>
#endif
#if !defined(POWERSUB_HPP)
#	include <powersub.hpp>
#endif

#include <stdio.h>		// sscanf (offline streamer)
#if !defined(RANDOM_HPP)
#	include <random.hpp>	// the engine's global RandomGenerator  Random
#endif

//===========================================================================//
// Reconstruction stand-ins LOCAL to this translation unit.
//   The per-zone damage code reaches engine clock / RNG / segment-tree / NULL
//   subsystem facets through an invented name set; these behaviour-neutral
//   stand-ins (and proxy casts onto the real Subsystem* / NotationFile* /
//   ResourceFile*) carry that surface so the recovered logic compiles.
//===========================================================================//
static struct ReconClock { Scalar CurrentTick() { return 0; } } TheTime;	// FUN_00414b60
static const Scalar TicksPerSecond = 1.0f;								// DAT_0052140c

struct DZRef			{ int index; };
struct SegmentRecord	{ int siblings; int children; };				// mech segment-tree node
struct SegmentIterator	{ template<class A> SegmentIterator(const A &) {} DZRef *Next() { return 0; } };
struct SegTableX		{ SegmentRecord *operator[](int) { return 0; } };	// cast of EntitySegment::SegmentTable

// Subsystem facets the recovered code reaches that the engine Subsystem does
// not expose under these names (cast of Subsystem*).
struct SubProxy2
{
	Scalar			ApplyDamage(Damage &)	{ return 0; }	// FUN_004ac07c
	void			OnDestroyed()			{}				// vtable slot 0x34
	int				isVital;						// +0x39
	AlarmIndicator	failureAlarm;					// +0x2c
	int				hasDestructor;					// +0x41
	void			*damageZone;					// +0xE0
};

// Offline .dmg/.skl notation readers (cast of NotationFile* / ResourceFile*).
struct NotationEntry	{ const char *name; const char *text; NotationEntry *Next() { return 0; } };
struct NotationList		{ int Count() { return 0; } NotationEntry *First() { return 0; } };
struct NoteX2											// cast of NotationFile*
{
	template<class...A> int			ReadLogical(A&&...)	{ return 0; }
	template<class...A> int			ReadString(A&&...)	{ return 0; }
	template<class...A> NotationList	FindList(A&&...)	{ return NotationList(); }
	void							Close()				{}
};
struct RFileX2			{ template<class A> NotationFile *Open(A) { return 0; } };	// cast of ResourceFile*

inline int	Str_Equal(const char *a, const char *b)	{ return a && b && strcmp(a, b) == 0; }
template<class...A> inline int FindSegment(A&&...)	{ return -1; }				// FUN_004274f8

//
// Sentinel for "no zone".  Returned by GetSegmentIndex/RandomRedirect on a miss.
//
const int Mech__DamageZone::NullDamageZone = -1;

// LOD reuse window: within [0.25s, 10.0s] of the previous hit, the artifact
// zone tends to keep routing to the same child (damage clustering).
static const Scalar LODReuseWindowMin = 0.25f;		// _DAT_0049c50c
static const Scalar LODReuseWindowMax = 10.0f;		// _DAT_0049c510

// Structure level is always clamped to [0,1].
static const Scalar StructureMin = 0.0f;			// _DAT_0049c5f8 / _DAT_0049c9a4 / _DAT_0049cacc
static const Scalar StructureMax = 1.0f;			// _DAT_0049c5fc / _DAT_0049c99c / _DAT_0049cad0

// A leg whose zone is half-gone already trips the partial leg-failure graphic
// when the mech is not in a normal standing/walking stance (the oracle reads
// _DAT_0049c9a0 == 0.5 in that branch, vs StructureMax (1.0) for the full
// "leg destroyed -> fall" case).
static const Scalar LegHalfStructure = 0.5f;		// _DAT_0049c9a0

// CriticalHit passes only half of the inflicted damage on to the armour model.
static const Scalar CriticalDamageFraction = 0.5f;	// _DAT_0049ce48
static const Scalar CriticalDamageMax      = 1.0f;	// _DAT_0049ce4c

// Probability of re-using the previous child zone inside the reuse window.
// (Ghidra read the operand at @0049c514; exact value not cleanly recoverable
// from the pseudo-C -- it behaves as a high "stay on the same spot" bias.)
static const Scalar LODReuseHysteresis = 0.82f;		// _DAT_0049c514 (TODO: verify)


//###########################################################################
//###########################################################################
//					MechCriticalSubsystem
//###########################################################################
//###########################################################################

//
// @0049dd18 -- owner is the DamageZone that will destroy this subsystem when
// the zone's structure runs out.  damagePercentage / damagePercentageUsed and
// criticalWeight are filled in afterwards by the streaming ctor of the zone.
//
MechCriticalSubsystem::MechCriticalSubsystem(DamageZone *owner)
	: subsystemPlug(owner)						// FUN_0049dd7e (SlotOf<Subsystem*>)
{
	criticalWeight       = 0;					// Wword(6) @0x18
	damagePercentageUsed = 0;					// Wword(5) @0x14
}

//
// @0049dd44 -- releases the subsystem slot (FUN_0049dd9d).
//
MechCriticalSubsystem::~MechCriticalSubsystem()
{
}

//
// @0049dd74
//
Logical
	MechCriticalSubsystem::TestInstance() const
{
	return True;
}


//###########################################################################
//###########################################################################
//					Mech::DamageZone
//###########################################################################
//###########################################################################

//#############################################################################
// Construction -- @0049ce50
//
//	Chains to the base DamageZone (which parses armour/structure from the .dmg
//	record and allocates the redirect table), then reads this class's own
//	stream fields, normalises the per-facing armour scalars, builds the
//	critical-subsystem array and finally the LOD redirect table.
//
Mech__DamageZone::Mech__DamageZone(
	Mech			*mech,
	int				damage_zone_index,
	MemoryStream	*stream
)
	: DamageZone(mech, damage_zone_index, stream)		// FUN_0041df5c
{
	// vtable @0050bb90
	redirectTable.Construct(0, 1);						// Wword(0x58) = FUN_00424767(,0,1)

	// last-redirect bookkeeping
	lastDamageTime = (Scalar)TheTime.CurrentTick() / TicksPerSecond;	// Wword(0x5f)
	lastInflicting = mech->entityID;					// Wword(0x60) = FUN_00420ea4(,mech+0x184)
	parentArtifactZone.Construct(0);					// Wword(0x62) = FUN_0049ddc9(,0)
	criticalWeightSum = 0;								// Wword(0x6c)

	//
	// Read the streamed scalar/flag block.  (Stream order set by
	// CreateStreamedDamageZone below.)
	//
	stream->ReadBytes(&descendOnDestruction, 4);				// Wword(0x67)
	stream->ReadBytes(&destroySiblingsOnDestruction, 4);		// Wword(0x68)
	stream->ReadBytes(&segmentIndex, 4);						// Wword(0x65)
	stream->ReadBytes(&leftLeg, 4);							// Wword(0x69)
	stream->ReadBytes(&rightLeg, 4);							// Wword(0x6a)
	stream->ReadBytes(&vitalDamageZone, 4);					// Wword(0x66)
	stream->ReadBytes(&criticalSubsystemCount, 4);			// Wword(0x6b)

	//
	// Normalise the damage-scale vector damageScale[Damage::DamageTypeCount]
	// (this[0x51..0x55], == base DamageZone::damageScale[5]).  These are NOT
	// per-facing: the base TakeDamage indexes them by Damage::damageType
	// (it reads this[0x51 + damage.damageType]; see DamageZone::TakeDamage
	// @0041e4e0), so the 5 cells are the {Collision,Ballistic,Explosive,Laser,
	// Energy} per-type armour scale for this zone.  Each cell is rewritten so a
	// stored "armour points" figure becomes a damage-per-point fraction
	// (1/(points*count)); a leg zone then halves every type's scale.
	//
	// damageScale[5] and defaultArmorPoints are the INHERITED engine DamageZone base
	// members (read raw from the stream by the base ctor, DAMAGE.cpp:301-307); we
	// normalise them in place here -- not a raw 0x144/0x140 offset (which would be the
	// binary layout, wrong when compiled against the engine base).
	for (int type = 0; type < Damage::DamageTypeCount; ++type)	// 5 damage types
	{
		Scalar even = 1.0f / defaultArmorPoints;		// _DAT_0049d1c4 / count
		if (fabsf(damageScale[type] - even) > 1.0e-4f)	// _DAT_0049d1c8
			damageScale[type] = 1.0f / (damageScale[type] * defaultArmorPoints);
		if (rightLeg || leftLeg)
			damageScale[type] *= 0.5f;					// _DAT_0049d1cc
	}

	//
	// Critical-subsystem table.
	//
	if (criticalSubsystemCount == 0)
	{
		criticalSubsystems = 0;							// Wword(0x6d)
	}
	else
	{
		criticalSubsystems =
			(MechCriticalSubsystem **)Memory::AllocateArray(criticalSubsystemCount * 4);
		for (int i = 0; i < criticalSubsystemCount; ++i)
		{
			criticalSubsystems[i] =
				new (Memory::Allocate(0x1c)) MechCriticalSubsystem(this);	// FUN_0049dd18
		}

		int subsystemIndex = -1;
		for (int i = 0; i < criticalSubsystemCount; ++i)
		{
			stream->ReadBytes(&criticalSubsystems[i]->criticalWeight, 4);		// +0x18
			stream->ReadBytes(&criticalSubsystems[i]->damagePercentage, 4);		// +0x10
			criticalWeightSum += criticalSubsystems[i]->criticalWeight;
			stream->ReadBytes(&subsystemIndex, 4);				// subsystem-table index

			// Wire the subsystem plug to the live subsystem unless this is the
			// pure-LOD / cosmetic build (mech flags 0x28 bits).
			if ((mech->flags & 0xc) == 0 && (mech->flags & 0x100) != 0)
				criticalSubsystems[i]->subsystemPlug.Resolve();		// FUN_00417ab4
		}
	}

	//
	// LOD redirect table: <count> followed by <count> child zone indices.
	//
	int redirectCount = 0;
	stream->ReadBytes(&redirectCount, 4);
	for (int i = 0; i < redirectCount; ++i)
	{
		int childZone = 0;
		stream->ReadBytes(&childZone, 4);
		redirectTable.Add(new (Memory::Allocate(0x10)) IntegerPlug(&childZone));	// FUN_00424716
	}

	lastHitZone = (redirectCount == 0) ? 0 : RandomRedirect();		// Wword(0x5e)
}

//#############################################################################
// SetLODParentPointers -- @0049d1d0
//
//	Run after every zone of the mech exists.  For each child index in this
//	(artifact) zone's redirect table, point that real child zone's
//	parentArtifactZone slot back at us.
//
void
	Mech__DamageZone::SetLODParentPointers()
{
	Mech *mech = (Mech *)GetOwningSimulation();
	DamageZoneIndexTableIterator it(&redirectTable);		// FUN_00424830
	for (IntegerPlug *plug; (plug = it.Next()) != 0; )		// slot 0x28
	{
		Mech__DamageZone *child = mech->Zone(plug->value);	// inherited Entity::damageZones[idx], typed
		child->parentArtifactZone = this;					// slot @0x188 +4
	}
}

//#############################################################################
// Destruction -- @0049d23c (vtable slot 0)
//
Mech__DamageZone::~Mech__DamageZone()
{
	*(void **)this = &vtable_0050bb90;

	if (criticalSubsystems != 0)
	{
		for (int i = 0; i < criticalSubsystemCount; ++i)
			delete criticalSubsystems[i];
		Memory::Free(criticalSubsystems);
	}

	{
		DamageZoneIndexTableIterator it(&redirectTable);	// FUN_00424830
		it.DeleteContents();								// FUN_00417858(,1)
	}
	// (binary tail: FUN_0049dde8 parentArtifactZone.~DZSlot, FUN_0042478a
	// redirectTable.~DamageZoneIndexTable, then base ~DamageZone FUN_0041e394 --
	// all COMPILER-EMITTED dtor epilogue glue; C++ re-emits member + base
	// destruction implicitly at the closing brace, so writing them as source runs
	// them twice.  See the reconstruction rule in mech.cpp ~Mech.)
}


//#############################################################################
// TakeDamage -- @0049c690 (vtable slot 6)
//
//	If this zone is an artifact (has a redirect table) the hit is handed to the
//	LOD router so it lands on a real child zone.  Otherwise the base DamageZone
//	applies it to armour/structure, the parent artifact (if any) recomputes its
//	displayed level, special damage may short an attached generator, and the
//	zone updates its graphic / leg-failure state.
//
//	The base armour model (DamageZone::TakeDamage @0041e4e0, == the WinTesla
//	engine DAMAGE.cpp body we link against) is:
//
//	    damageLevel += damage.damageAmount * damageScale[damage.damageType];
//	    Clamp(damageLevel, 0.0f, 1.0f);            // _DAT_0041e57c / _DAT_0041e580
//	    SetDamageLevelChangedFlag();               // flags |= 4
//	    if (damageLevel >= 1.0f) {                 // zone destroyed
//	        SetDamageZoneState(BurningState);      // state indicator -> 1
//	        SetGraphicState(DestroyedGraphicState);// vtable slot 3 -> 1
//	        SetGraphicStateChangedFlag();          // flags |= 8
//	    }
//
//	i.e. there is no separate "armour then structure" pool -- a single 0..1
//	damageLevel accumulates, scaled per damage TYPE by damageScale[5] (the
//	vector normalised in the ctor above); 1.0 == destroyed.  (damageLevel in
//	this reconstruction names that same base damageLevel cell @0x158.)
//
void
	Mech__DamageZone::TakeDamage(Damage& damage)
{
	Mech *mech = (Mech *)GetOwningSimulation();

	DamageZoneIndexTableIterator it(&redirectTable);		// FUN_00424830
	if (it.Count() != 0)									// slot 0x14 -> artifact zone
	{
		EntityID inflicting = mech->lastInflictingID;		// mech[0x43c]
		Damage local = damage;								// copy 10 words
		LODDamageRouter(inflicting, local);					// FUN_0049c40c
		return;
	}

	// ---- real zone: apply to armour/structure ----
	DamageZone::TakeDamage(damage);							// FUN_0041e4e0

	Mech__DamageZone *parent = parentArtifactZone.Resolve();	// FUN_00417ab4(this+0x62)
	if (parent != 0)
		parent->UpdateLODDamageLevel();						// FUN_0049c51c

	// ---- ShortAttachedGenerators (inlined): special damage type 4 ----
	if (damage.damageType == 4 && criticalSubsystemCount > 0)
	{
		int n = Random(criticalSubsystemCount);	// FUN_0040807c
		Subsystem *s = criticalSubsystems[n]->subsystemPlug.Resolve();
		if (s->IsDerivedFrom(*Generator::GetClassDerivations()))	// FUN_0041a1a4(,0x50f4bc)
			((Generator *)s)->ForceShortRecovery();			// FUN_004b11bc
	}

	// ---- graphic / failure state from current structure level ----
	if (vitalDamageZone == 0 || damageLevel < StructureMax)
	{
		if (leftLeg == 0 && rightLeg == 0)
		{
			if (damageLevel >= StructureMax)
				RecurseSegmentTable(mech);					// FUN_0049cad4
		}
		else if ((unsigned)(mech->stance - 3) < 2)			// standing/walking
		{
			if (rightLeg != 0 && damageLevel >= StructureMax)
				mech->graphicAlarm.SetLevel(9);				// fall / leg gone
			else if (leftLeg != 0 && damageLevel >= StructureMax)
				mech->graphicAlarm.SetLevel(9);
		}
		else if (mech->IsAirborne() == 0)					// FUN_0049fb54
		{
			// Non-standing stance, still grounded: a half-destroyed leg zone
			// trips the partial-failure graphic (right -> 4, left -> 3).  The
			// oracle compares against _DAT_0049c9a0 (0.5), NOT StructureMax.
			if (rightLeg != 0 && damageLevel >= LegHalfStructure)
				mech->graphicAlarm.SetLevel(4);
			else if (leftLeg != 0 && damageLevel >= LegHalfStructure)
				mech->graphicAlarm.SetLevel(3);
		}
	}
	else
	{
		// vital zone destroyed -> mech death
		SetDamageZoneState(1);							// this+4
		mech->graphicAlarm.SetLevel(9);
	}

	damageLevel = Clamp(damageLevel, StructureMin, StructureMax);
}

//#############################################################################
// SetGraphicState -- @0049c66c (vtable slot 3)
//
//	Drives the secondary "graphic state" alarm (this+0x64) and flags the zone
//	as needing a visual refresh (Wword(0xb8) |= 8).
//
// Mech__DamageZone::SetGraphicState removed -- byte-identical to the engine base
// DamageZone::SetGraphicState (damageZoneGraphicState.SetState(new_state) +
// SetGraphicStateChangedFlag(), the latter == the old `flags |= 8`).  Calls below
// resolve to the inherited base override.

//#############################################################################
// LODDamageRouter -- @0049c40c
//
//	An artifact zone decides which real child takes the hit.  If the same
//	attacker hit recently (within [0.25s, 10.0s]) and a random roll succeeds
//	the previous child is reused; otherwise a fresh RandomRedirect() is chosen.
//	A brand-new attacker always re-rolls.
//
void
	Mech__DamageZone::LODDamageRouter(EntityID inflicting, Damage damage)
{
	int zone;
	if (inflicting != lastInflicting)						// FUN_0042104c
	{
		zone = RandomRedirect();							// new attacker
	}
	else
	{
		Scalar dt = (Scalar)TheTime.CurrentTick() / TicksPerSecond - lastDamageTime;
		if (dt < LODReuseWindowMin)
			zone = lastHitZone;								// too soon: same spot
		else if (dt > LODReuseWindowMax)
			zone = RandomRedirect();						// stale: re-roll
		else if ((Scalar)Random <= LODReuseHysteresis)	// _DAT_0049c514
			zone = lastHitZone;
		else
			zone = RandomRedirect();
	}

	lastHitZone    = zone;									// Wword(0x5e)
	lastDamageTime = (Scalar)TheTime.CurrentTick() / TicksPerSecond;
	lastInflicting = inflicting;							// FUN_00420ef4

	Mech *mech = (Mech *)GetOwningSimulation();
	mech->Zone(zone)->TakeDamage(damage);				// slot 6 on the real child
	UpdateLODDamageLevel();									// FUN_0049c51c
}

//#############################################################################
// RandomRedirect -- @0049c600
//
//	Uniformly pick one of the redirect table's child zone indices.
//
int
	Mech__DamageZone::RandomRedirect()
{
	DamageZoneIndexTableIterator it(&redirectTable);		// FUN_00424830
	int n = Random(it.Count());					// FUN_0040807c(rng, slot 0x14)
	IntegerPlug *plug = it[n];								// slot 0x34
	return plug->value;										// +0xc
}

//#############################################################################
// UpdateLODDamageLevel -- @0049c51c
//
//	An artifact zone's displayed structure is the mean of its real children's
//	structure levels, clamped to [0,1].
//
void
	Mech__DamageZone::UpdateLODDamageLevel()
{
	Mech *mech = (Mech *)GetOwningSimulation();
	unsigned count = 0;
	Scalar    sum  = 0.0f;

	DamageZoneIndexTableIterator it(&redirectTable);		// FUN_00424830
	for (IntegerPlug *plug; (plug = it.Next()) != 0; )		// slot 0x28
	{
		sum += mech->Zone(plug->value)->damageLevel;	// +0x158
		++count;
	}

	damageLevel = (count != 0) ? sum / (Scalar)count : StructureMin;
	damageLevel = Clamp(damageLevel, StructureMin, StructureMax);
}


//#############################################################################
// CriticalHit -- @0049ccc4
//
//	Apply (half of) a critical hit.  Half the damage amount flows through the
//	normal armour model (TakeDamage); the remainder rolls one critical
//	subsystem, weighted by criticalWeight, and damages it.  Returns the chosen
//	subsystem (or 0 if none was selected).
//
Subsystem*
	Mech__DamageZone::CriticalHit(Damage &damage_data)
{
	Scalar bite = damage_data.damageAmount * CriticalDamageFraction;	// damage+4 * 0.5
	if (bite > CriticalDamageMax)
		bite = CriticalDamageMax;
	damage_data.damageAmount -= bite;

	TakeDamage(damage_data);									// slot 6 (this+0x18)

	Scalar roll = (Scalar)Random;								// FUN_00408050
	if (criticalSubsystems != 0)
	{
		for (int i = 0; i < criticalSubsystemCount; ++i)
		{
			Scalar cum = criticalSubsystems[i]->criticalWeight;
			for (int j = 0; j < i; ++j)
				cum += criticalSubsystems[j]->criticalWeight;

			if (roll <= cum / criticalWeightSum)
			{
				Subsystem *s = criticalSubsystems[i]->subsystemPlug.Resolve();
				MechCriticalSubsystem *cs = criticalSubsystems[i];
				if (cs->damagePercentageUsed < cs->damagePercentage)
					cs->damagePercentageUsed += ((SubProxy2 *)s)->ApplyDamage(damage_data);	// FUN_004ac07c
				return criticalSubsystems[i]->subsystemPlug.Resolve();
			}
		}
	}
	return 0;
}

//#############################################################################
// SendSubsystemDamage -- @0049c9a8
//
//	When this zone is destroyed it forces full structure (1.0) on itself and
//	pushes each critical subsystem's outstanding damage fraction onto the zone
//	that physically carries that subsystem; any zone driven to destruction
//	there raises its alarms and fails the subsystem.
//
void
	Mech__DamageZone::SendSubsystemDamage()
{
	SetDamageZoneState(1);									// this+0x10
	damageLevel = StructureMax;								// this+0x158 = 1.0

	for (int i = 0; i < criticalSubsystemCount; ++i)
	{
		Subsystem        *s  = criticalSubsystems[i]->subsystemPlug.Resolve();	// +4
		MechCriticalSubsystem *cs = criticalSubsystems[i];

		// UNBOUND-PLUG GUARD.  Resolve() (FUN_00417ab4) returns the subsystem bound
		// to the DZSlot, or 0 if plug+8 was never wired.  In the 1995 binary every
		// critical subsystem exists and is bound, so it dereferences s with no check.
		// In this reconstruction a critical subsystem can be UNBOUND (its type is not
		// yet built, or its slot never connected), leaving s == 0 -> the original
		// code AV'd here (mov [edx+0xc], edx=0) as soon as a zone with such a plug was
		// destroyed.  Skip the unbound entry rather than crash; log it so the missing
		// binding can be tracked.  NOT a behavioural stand-in: a bound plug takes the
		// authentic path below unchanged.  [T3 -- see open-questions: crit-subsys binding]
		if (s == 0)
		{
			if (getenv("BT_DEATH_LOG"))
				DEBUG_STREAM << "[deathfx] crit-subsys " << i << "/" << criticalSubsystemCount
					<< " UNBOUND (plug not wired) -- skipped\n" << std::flush;
			continue;
		}

		Mech__DamageZone *zone = (Mech__DamageZone *)((SubProxy2 *)s)->damageZone;	// subsystem[0x38] @0xE0

		zone->damageLevel += (cs->damagePercentage - cs->damagePercentageUsed);
		zone->damageLevel = Clamp(zone->damageLevel, StructureMin, StructureMax);

		if (zone->damageLevel >= StructureMax)				// destroyed
		{
			if (((SubProxy2 *)s)->isVital)						// subsystem[0x39]
				((Mech *)GetOwningSimulation())->graphicAlarm.SetLevel(9);
			((SubProxy2 *)s)->failureAlarm.SetLevel(1);			// subsystem+0xb
			if (((SubProxy2 *)s)->hasDestructor)				// subsystem[0x41]
				((SubProxy2 *)s)->OnDestroyed();				// slot 0x34
			zone->SetDamageZoneState(1);						// zone+0x10
		}
	}
}

//#############################################################################
// RecurseSegmentTable -- @0049cad4
//
//	Walks the model's segment tree from this zone.  First pushes this zone's
//	subsystem damage out (SendSubsystemDamage), then -- per the two streamed
//	flags -- destroys sibling zones (destroySiblingsOnDestruction) and/or
//	descends into child segments (descendOnDestruction), recursing on each.
//
void
	Mech__DamageZone::RecurseSegmentTable(Mech *my_mech)
{
	SendSubsystemDamage();										// FUN_0049c9a8
	SetGraphicState(1);											// slot 0xc

	SegmentRecord *seg = ((SegTableX &)my_mech->segmentTable)[segmentIndex];	// mech+0x300, Wword(0x65)

	if (destroySiblingsOnDestruction)							// Wword(0x68)
	{
		SegmentIterator sibs(&seg->siblings);					// seg+0xd0
		for (DZRef *r; (r = sibs.Next()) != 0; )				// slot 0x28
		{
			Mech__DamageZone *sib = my_mech->Zone(r->index);	// inherited damageZones[..], typed
			if (sib != this && sib->GetGraphicState() != 1)
			{
				sib->RecurseSegmentTable(my_mech);
				sib->SetGraphicState(2);						// slot 0xc
			}
		}
	}

	if (descendOnDestruction)									// Wword(0x67)
	{
		SegmentIterator kids(&seg->children);					// seg+0xe8
		for (DZRef *r; (r = kids.Next()) != 0; )
		{
			SegmentRecord *child = ((SegTableX &)my_mech->segmentTable)[r->index];
			SegmentIterator czones(&child->siblings);			// child+0xd0
			for (DZRef *cr; (cr = czones.Next()) != 0; )
			{
				Mech__DamageZone *cz = my_mech->Zone(cr->index);
				cz->RecurseSegmentTable(my_mech);
				cz->SetGraphicState(2);
			}
		}
	}
}


//#############################################################################
// CreateStreamedDamageZone -- @0049d304
//
//	Parse ONE zone out of the .dmg notation file into the memory stream that
//	the ctor above will later replay.  Called once per entry in the .dmg
//	"DamageZones" list by Mech::CreateDamageZoneStream (@004a474c).
//
//	Token stream written, in order:
//	   (base fields via DamageZone::CreateStreamedDamageZone @0041e590)
//	   DescendOnDestruction (Logical)
//	   DestroySiblingsOnDestruction (Logical)
//	   GetSegmentIndex(name, skl)   (int)
//	   leftLeg, rightLeg            (from "LegDamageZone" == LeftLeg/RightLeg)
//	   vitalDamageZone              (from "VitalDamageZone" == True/False, required)
//	   criticalSubsystemCount, then per "CriticalSubsystem <name> <%f> <%f>":
//	        subsystem index ("ControlsMapper"->0, "Power"->1, else Subsystems list),
//	        the two percentages
//	   redirectCount, then per "LODRedirect" entry: GetSegmentIndex of the name.
//
Logical
	Mech__DamageZone::CreateStreamedDamageZone(
		ResourceFile	*resource_file,
		NotationFile	*model_file,
		const char		*model_name,
		NotationFile	*skl_file,
		const char		*damage_zone_name,
		NotationFile	*dmg_file,
		const ResourceDirectories	*directories,
		MemoryStream	*damage_zone_stream
	)
{
	CString name(damage_zone_name);

	// base armour/structure record
	if (!DamageZone::CreateStreamedDamageZone(				// FUN_0041e590
			model_file, model_name, skl_file, name, damage_zone_stream, dmg_file, directories))
		return (Logical)-1;

	Logical descend = 0;
	((NoteX2 *)dmg_file)->ReadLogical(damage_zone_name, "DescendOnDestruction", &descend);
	damage_zone_stream->WriteBytes(&descend, 4);

	Logical destroySibs = 0;
	((NoteX2 *)dmg_file)->ReadLogical(damage_zone_name, "DestroySiblingsOnDestruction", &destroySibs);
	damage_zone_stream->WriteBytes(&destroySibs, 4);

	int segIndex = GetSegmentIndex(name, skl_file);			// FUN_0049db20
	damage_zone_stream->WriteBytes(&segIndex, 4);

	Logical isLeftLeg = 0, isRightLeg = 0;
	const char *leg;
	if (((NoteX2 *)dmg_file)->ReadString(damage_zone_name, "LegDamageZone", &leg))
	{
		if (Str_Equal(leg, "RightLeg"))	isRightLeg = 1;
		if (Str_Equal(leg, "LeftLeg"))	isLeftLeg  = 1;
	}
	damage_zone_stream->WriteBytes(&isLeftLeg, 4);
	damage_zone_stream->WriteBytes(&isRightLeg, 4);

	const char *vital;
	if (!((NoteX2 *)dmg_file)->ReadString(damage_zone_name, "VitalDamageZone", &vital))
	{
		DebugStream << dmg_file << damage_zone_name << " missing VitalDamageZone!";
		return False;
	}
	Logical isVital;
	if (Str_Equal(vital, "True"))		isVital = 1;
	else if (Str_Equal(vital, "False"))	isVital = 0;
	else
	{
		DebugStream << model_name << " " << damage_zone_name
					<< " Improper format for LegDamageZone";
		return False;
	}
	damage_zone_stream->WriteBytes(&isVital, 4);

	//
	// CriticalSubsystem list ("<name> <%f> <%f>").
	//
	NotationList crits = ((NoteX2 *)dmg_file)->FindList(damage_zone_name, "CriticalSubsystem");
	int critCount = crits.Count();
	damage_zone_stream->WriteBytes(&critCount, 4);
	for (NotationEntry *e = crits.First(); e != 0; e = e->Next())
	{
		char   subName[64];
		Scalar pctA, pctB;
		if (sscanf(e->text, "%s %f %f", subName, &pctA, &pctB) == 0)	// s__s__f__f
		{
			DebugStream << model_name << " error in CriticalSubsystem Format!";
			return (Logical)-1;
		}
		damage_zone_stream->WriteBytes(&pctA, 4);
		damage_zone_stream->WriteBytes(&pctB, 4);

		int subIndex;
		if (Str_Equal(subName, "ControlsMapper"))
			subIndex = 0;
		else if (Str_Equal(subName, "Power"))
			subIndex = 1;
		else
		{
			// search the model's "Subsystems" list, indices start at 2
			const char *subFile;
			((NoteX2 *)model_file)->ReadString("gamedata", "Subsystems", &subFile);
			NotationFile *sf = ((RFileX2 *)resource_file)->Open(subFile);
			NotationList  list = ((NoteX2 *)sf)->FindList(0);
			subIndex = 2;
			NotationEntry *se;
			for (se = list.First(); se != 0; se = se->Next(), ++subIndex)
				if (Str_Equal(se->name, subName))
					break;
			((NoteX2 *)sf)->Close();
			if (se == 0)
			{
				DebugStream << model_name << ':' << subName << " does not exist!";
				return (Logical)-1;
			}
		}
		damage_zone_stream->WriteBytes(&subIndex, 4);
	}

	//
	// LODRedirect list: each redirect names a child zone resolved via the
	// segment table (TableOf::Find @004274f8); -1 == not found is an error.
	//
	NotationList redirects = ((NoteX2 *)dmg_file)->FindList(damage_zone_name, "LODRedirect");
	int redirectCount = redirects.Count();
	damage_zone_stream->WriteBytes(&redirectCount, 4);
	for (NotationEntry *e = redirects.First(); e != 0; e = e->Next())
	{
		CString childName(e->text);
		if (childName.Length() == 0)
		{
			DebugStream << model_name << " error in LODRedirect Format!";
			return (Logical)-1;
		}
		int childIndex = FindSegment(childName, skl_file);		// FUN_004274f8
		if (childIndex == -1)
		{
			DebugStream << resource_file << ' ' << childName << " Not Found!";
			return (Logical)-1;
		}
		damage_zone_stream->WriteBytes(&childIndex, 4);
	}

	return True;
}

//#############################################################################
// GetSegmentIndex -- @0049db20
//
//	Walk the skeleton (.skl) notation file's segment groups in order.  Skipping
//	the "LAB_ONLY" and "DamageZones" pseudo-groups, count real segments; for
//	each segment read its "dzone" sub-list and, if any dzone name matches
//	damage_zone_name, return the running segment index.  Returns -1 on miss.
//
int
	Mech__DamageZone::GetSegmentIndex(CString damage_zone_name, NotationFile *skl_file)
{
	int index = -1;
	NotationList segments = ((NoteX2 *)skl_file)->FindList(0);				// FUN_0040485c
	for (NotationEntry *seg = segments.First(); seg != 0; seg = seg->Next())
	{
		if (Str_Equal(seg->name, "LAB_ONLY"))   continue;
		if (Str_Equal(seg->name, "DamageZones")) continue;

		++index;
		NotationList dzones = ((NoteX2 *)skl_file)->FindList(seg->name, "dzone");	// FUN_00404720
		for (NotationEntry *dz = dzones.First(); dz != 0; dz = dz->Next())
		{
			CString dzName(dz->text);
			if (dzName == damage_zone_name)						// FUN_004027d8
				return index;
		}
	}
	return -1;
}

//#############################################################################
// Damage-state descriptor table -- @0041e4a8 / @0042a748 / @0042a2c8
//
//	Streams this zone's damage-state descriptor table from the type-0x1e resource
//	(binary this+0xd4).  Each entry (0x1C bytes) is [f32 DamageLevel][i32
//	EffectResource][i32 GraphicState][f32 TimeDelay], written ascending by
//	DamageLevel.  (Was a no-op stub -> the table was never built, so
//	MechDeathHandler had nothing to walk and death was invisible.)
//#############################################################################
void
	Mech__DamageZone::LoadCriticalSubsystems(MemoryStream *stream)		// @0041e4a8
{
	int count = 0;
	stream->ReadBytes(&count, 4);										// FUN_0042a748: entry count
	damageDescriptors.reserve(count > 0 ? count : 0);
	for (int i = 0; i < count; ++i)										// FUN_0042a2c8 per entry
	{
		DamageDescriptor d;
		stream->ReadBytes(&d.damageLevel,    4);					// +0xc
		stream->ReadBytes(&d.effectResource, 4);					// +0x14
		stream->ReadBytes(&d.graphicState,   4);					// +0x10
		stream->ReadBytes(&d.timeDelay,      4);					// +0x18
		damageDescriptors.push_back(d);
	}
	if (count != 0 && getenv("BT_DEATH_LOG"))
		DEBUG_STREAM << "[deathfx] zone descriptor table loaded: " << count
			<< " entries (first threshold=" << (count > 0 ? damageDescriptors[0].damageLevel : -1.0f)
			<< " effect=" << (count > 0 ? damageDescriptors[0].effectResource : -1) << ")\n" << std::flush;
}

//
// @0042a664 -- first entry whose threshold is strictly greater than 'level'
// (0 if none / the zone is past the last threshold, i.e. fully destroyed).
//
const Mech__DamageZone::DamageDescriptor *
	Mech__DamageZone::DescriptorForLevel(Scalar level) const
{
	for (unsigned i = 0; i < damageDescriptors.size(); ++i)
		if (damageDescriptors[i].damageLevel > level)
			return &damageDescriptors[i];
	return 0;
}

//
// @0042a5f4 -- true iff some entry's threshold lies in (oldLevel, newLevel]
// (a damage-state threshold was crossed this tick).
//
Logical
	Mech__DamageZone::DescriptorCrossed(Scalar oldLevel, Scalar newLevel) const
{
	for (unsigned i = 0; i < damageDescriptors.size(); ++i)
	{
		Scalar t = damageDescriptors[i].damageLevel;
		if (t > oldLevel && t <= newLevel)
			return True;
	}
	return False;
}

//
// @0042a6c4 -- first entry whose GraphicState enum == gstate (0 if none).
//
const Mech__DamageZone::DamageDescriptor *
	Mech__DamageZone::DescriptorForGraphicState(int gstate) const
{
	for (unsigned i = 0; i < damageDescriptors.size(); ++i)
		if (damageDescriptors[i].graphicState == gstate)
			return &damageDescriptors[i];
	return 0;
}

// Inherited engine DamageZone::damageLevel (binary this+0x158) -- the [0,1]
// structural damage MechDeathHandler compares against the descriptor thresholds.
Scalar
	Mech__DamageZone::GetStructureDamageLevel() const
{
	return damageLevel;
}

void
	Mech__DamageZone::ApplyDamageGraphicState(int gstate)
{
	SetGraphicState(gstate);			// engine DamageZone::SetGraphicState -> destroyed skin
}

//#############################################################################
// MechDeathHandler -- @0042a984 / @0042aa2c / @0042a9f4
//#############################################################################

// Effect-spawn bridge (mech4.cpp -- where Explosion::Make + the mech transform
// live): spawns the descriptor's explosion resource at the mech.  The authentic
// path dispatches a class-5 message to the effect manager (app+0x38 -> the 0xBD3
// SubsystemMessageManager, unreconstructed); we use the established Explosion port.
extern void BTSpawnDamageEffect(Mech *mech, int effect_resource, int segment_index);

// Render bridge (btl4vid.cpp): swap a mech's wrecked segment meshes onto its
// already-built render tree when a damage zone's graphic state changes (the
// unported "RemakeEntity" render state).  Entity* param -> Mech* binds fine.
extern void BTRemakeMechModel(Entity *entity);

MechDeathHandler::MechDeathHandler(Mech *mech)			// @0042a984
	: owner(mech)
{
	// per-zone last-damage cache, zeroed (binary this[0x10], size mech+0x11c).
	int count = (mech != 0 && mech->damageZoneCount > 0) ? mech->damageZoneCount : 0;
	lastLevel.assign(count, 0.0f);
}

MechDeathHandler::~MechDeathHandler()					// @0042a9f4
{
}

//
// @0042aa2c -- each tick, walk the zones; where a zone's damageLevel rose since
// last tick, fire the current damage-band descriptor's explosion (binary +0xb8 &
// 4) and, on destruction, the Destroyed-graphic descriptor's explosion (+0xb8 &
// 8), applying that descriptor's GraphicState (the destroyed skin) to the zone.
//
void
	MechDeathHandler::Tick()							// @0042aa2c
{
	if (owner == 0)
		return;

	int n = owner->damageZoneCount;
	for (int i = 0; i < n && i < (int)lastLevel.size(); ++i)
	{
		Mech__DamageZone *zone = owner->Zone(i);
		if (zone == 0)
			continue;

		Scalar level = zone->GetStructureDamageLevel();
		Scalar prev  = lastLevel[i];
		if (level <= prev)								// no new damage on this zone
			continue;
		lastLevel[i] = level;

		const Mech__DamageZone::DamageDescriptor *d = zone->DescriptorForLevel(level);
		if (level >= 1.0f && prev < 1.0f)				// just destroyed -> the Destroyed descriptor
		{
			const Mech__DamageZone::DamageDescriptor *dd =
				zone->DescriptorForGraphicState(DamageZone::DestroyedGraphicState);	// enum 1
			if (dd != 0)
				d = dd;
		}
		if (d != 0)
		{
			BTSpawnDamageEffect(owner, d->effectResource,	// explosion AT the zone's
					zone->segmentIndex);					// segment (world position)
			zone->ApplyDamageGraphicState(d->graphicState);	// destroyed skin (graphic state)
			// A graphic-state change means the segment's MODEL changed (intact ->
			// destroyed variant, keyed by GetVideoObjectName(skl, gstate)).  Fire
			// the engine's model-rebuild flag so the renderer re-runs
			// MakeMechRenderables and loads the destroyed segment geometry -- the
			// same mechanism CulturalIcons use to visibly deform on damage
			// (CULTURAL.cpp:91; EXPTBL.cpp:549 does this per zone in the engine's
			// ExplosionTable).  Without this the tree stays as built at spawn (intact).
			// The render's "RemakeEntity" state (which consumes this flag) was
				// NOT ported -- the mech tree is built once at spawn -- so we drive the
				// equivalent directly: set the flag (authentic signal; also flags the
				// zone for net damage replication) AND call the render bridge, which
				// swaps the wrecked segment mesh onto the already-built tree in place.
				// The single-threaded frame loop makes the immediate swap safe.
				if (d->graphicState != DamageZone::ExistsGraphicState)
				{
					owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
					BTRemakeMechModel(owner);			// RemakeEntity: swap in the destroyed mesh
				}
			if (getenv("BT_DEATH_LOG"))
				DEBUG_STREAM << "[deathfx] zone " << i << " level " << level
					<< " -> effect " << d->effectResource << " gstate " << d->graphicState
					<< "\n" << std::flush;
		}
	}
}
