Mech::DamageZone::TakeDamage is the full binary cascade (@0049c690): LOD artifact router with same-attacker clustering (@0049c40c, hysteresis 0.33), artifact parent level = mean of children, Energy-hit generator shorts through the crit plug binding (novice-exempt), weighted CriticalHit (@0049ccc4), zone-death allotment push (SendSubsystemDamage @0049c9a8) and the segment-table destruction descent (RecurseSegmentTable @0049cad4 -- SIBS + DESCEND via the engine EntitySegment iterators). Supporting bricks: Mech's OWN handler table with the TakeDamage override (latches lastInflictingID @0x43c; gyro feed staged; cylinder table deferred); friend class Mech__DamageZone (authentic); subsystem private zones ARMED (structureReference + armorByFacing[5] normalized -- crits are measurable); ApplyDamageAndMeasure; ForceCriticalFailure sets DestroyedState so the weapon FSMs' GetSimulationState()==1 hard gate went live; Generator::ForceShort + PoweredSubsystem::ForceShortRecovery. Fight-verified (mutual BT_SPAWN_ENEMY + BT_FORCE_ZONE=8 concentrated fire): arm zone to 1.0 -> sibling searchlight zone dies (Searchlight2/ThermalSight crits destroyed, HUD degrading) -> DESCEND kills the gun zone -> PPC_2 + ERMLaser_2 + Condenser6 DESTROYED and FALL SILENT (0 shots after death; undamaged PPC_1 keeps firing); vital-zone hit = MECH KILL; PPC hit shorted GeneratorD. This art has zero artifact zones (redirect=0 across all 20 -- router verified dormant-correct). Smoke + novice regressions clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
776 lines
22 KiB
C++
776 lines
22 KiB
C++
//===========================================================================//
|
|
// File: mechdmg.cpp //
|
|
// Project: BattleTech Brick: Mech damage //
|
|
// Contents: Mech::DamageZone -- the hull zone armor economy //
|
|
//---------------------------------------------------------------------------//
|
|
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
|
// All Rights reserved worldwide //
|
|
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
|
//===========================================================================//
|
|
//
|
|
// The class interface is the AUTHENTIC surviving CODE/BT/BT/MECHDMG.HPP
|
|
// (06/05/95 JM). The bodies here are reconstructions: the streamed ctor /
|
|
// SetLODParentPointers / dtor follow the BT411-verified binary parse
|
|
// (@0049ce50 / @0049d1d0), while TakeDamage and SetGraphicState are STAGED
|
|
// pass-throughs to the engine base -- the LOD artifact router (@0049c690),
|
|
// the critical destruction cascade and the death-handler graphics are the
|
|
// mechdmg cascade wave (see MECHDMG.NOTES.md).
|
|
//
|
|
|
|
#include <bt.hpp>
|
|
#pragma hdrstop
|
|
|
|
#if !defined(MECHDMG_HPP)
|
|
# include <mechdmg.hpp>
|
|
#endif
|
|
|
|
#if !defined(MECH_HPP)
|
|
# include <mech.hpp>
|
|
#endif
|
|
|
|
#if !defined(MECHSUB_HPP)
|
|
# include <mechsub.hpp>
|
|
#endif
|
|
|
|
#if !defined(POWERSUB_HPP)
|
|
# include <powersub.hpp>
|
|
#endif
|
|
|
|
#if !defined(SEGMENT_HPP)
|
|
# include <segment.hpp>
|
|
#endif
|
|
|
|
#if !defined(RANDOM_HPP)
|
|
# include <random.hpp>
|
|
#endif
|
|
|
|
const int
|
|
Mech__DamageZone::NullDamageZone = -1;
|
|
|
|
//
|
|
// Cascade constants (binary data words, BT411-verified addresses).
|
|
//
|
|
static const Scalar LODReuseWindowMin = 0.25f; // _DAT_0049c50c
|
|
static const Scalar LODReuseWindowMax = 10.0f; // _DAT_0049c510
|
|
static const Scalar LODReuseHysteresis = 0.33f; // _DAT_0049c514 (double 0.33)
|
|
static const Scalar StructureMin = 0.0f; // _DAT_0049c5f8
|
|
static const Scalar StructureMax = 1.0f; // _DAT_0049c5fc
|
|
static const Scalar LegHalfStructure = 0.5f; // _DAT_0049c9a0
|
|
static const Scalar CriticalDamageFraction = 0.5f; // _DAT_0049ce48
|
|
static const Scalar CriticalDamageMax = 1.0f; // _DAT_0049ce4c
|
|
|
|
//
|
|
//#############################################################################
|
|
// MechCriticalSubsystem (binary ctor @0049dd18): one critical-subsystem
|
|
// entry of a hull zone -- the plug binds the roster subsystem this zone
|
|
// destroys when its structure runs out; weight / percentage stream in the
|
|
// zone ctor below.
|
|
//#############################################################################
|
|
//
|
|
MechCriticalSubsystem::MechCriticalSubsystem(DamageZone *owner):
|
|
subsystemPlug(owner)
|
|
{
|
|
Check(owner);
|
|
criticalWeight = 0.0f;
|
|
damagePercentage = 0.0f;
|
|
damagePercentageUsed = 0.0f;
|
|
}
|
|
|
|
MechCriticalSubsystem::~MechCriticalSubsystem()
|
|
{
|
|
}
|
|
|
|
Logical
|
|
MechCriticalSubsystem::TestInstance() const
|
|
{
|
|
return True;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// Mech::DamageZone construction (binary @0049ce50).
|
|
//
|
|
// Chains to the engine DamageZone streamed ctor (name / effect sites /
|
|
// defaultArmorPoints / damageScale[5] / material lists -- DAMAGE.CPP:234),
|
|
// then consumes the BT mech-specific TAIL of the same per-zone record:
|
|
//
|
|
// Logical descendOnDestruction, destroySiblingsOnDestruction;
|
|
// int segmentIndex;
|
|
// Logical leftLeg, rightLeg, vitalDamageZone;
|
|
// int criticalSubsystemCount;
|
|
// critCount x { Scalar criticalWeight; Scalar damagePercentage;
|
|
// int subsystemIndex -> subsystemPlug }
|
|
// int redirectCount; redirectCount x int childZoneIndex
|
|
//
|
|
// Parsing the FULL record is LOAD-BEARING: the Mech ctor streams all hull
|
|
// zones sequentially from ONE stream, so a short read here skews every
|
|
// following zone (its effect-site indices then read garbage and
|
|
// GetSegment() returns NULL -- the 5.3.16 bring-up crash).
|
|
//
|
|
// THE ARMOR ECONOMY (BT411 combat-damage, verified vs the binary): the
|
|
// stream authors armor POINTS; each damageScale cell is normalized to a
|
|
// damage-per-point fraction scale[type] = 1/(raw[type] * armorPoints)
|
|
// (cells already equal to 1/armorPoints pass through), then LEG zones halve
|
|
// every type's scale. The engine base TakeDamage applies
|
|
// damageLevel += amount * damageScale[type] (1.0 = destroyed).
|
|
//#############################################################################
|
|
//
|
|
Mech__DamageZone::Mech__DamageZone(
|
|
Mech *mech,
|
|
int damage_zone_index,
|
|
MemoryStream *stream
|
|
):
|
|
DamageZone(mech, damage_zone_index, stream),
|
|
redirectTable(NULL, False),
|
|
parentArtifactZone(NULL)
|
|
{
|
|
Check(mech);
|
|
Check(stream);
|
|
|
|
lastHitZone = 0;
|
|
lastDamageTime = 0.0f;
|
|
lastInflicting = mech->GetEntityID();
|
|
criticalWeightSum = 0.0f;
|
|
criticalSubsystems = NULL;
|
|
|
|
//
|
|
// The streamed scalar/flag block (binary order @0049cf00-0049cf5c).
|
|
//
|
|
*stream >> descendOnDestruction;
|
|
*stream >> destroySiblingsOnDestruction;
|
|
*stream >> segmentIndex;
|
|
*stream >> leftLeg;
|
|
*stream >> rightLeg;
|
|
*stream >> vitalDamageZone;
|
|
*stream >> criticalSubsystemCount;
|
|
|
|
//
|
|
// Normalize the per-type armor scales in place (binary @0049cf60,
|
|
// constants 1e-4 / 0.5).
|
|
//
|
|
{
|
|
for (int type = 0; type < Damage::DamageTypeCount; ++type)
|
|
{
|
|
Scalar even = 1.0f / defaultArmorPoints;
|
|
Scalar diff = damageScale[type] - even;
|
|
if (diff < 0.0f)
|
|
{
|
|
diff = -diff;
|
|
}
|
|
if (diff > 1.0e-4f)
|
|
{
|
|
damageScale[type] =
|
|
1.0f / (damageScale[type] * defaultArmorPoints);
|
|
}
|
|
if (leftLeg || rightLeg)
|
|
{
|
|
damageScale[type] *= 0.5f;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// The critical-subsystem table. The streamed roster index binds the
|
|
// subsystem plug on the MASTER mech only (binary gate @0049d0e1:
|
|
// MasterInstance + DynamicFlag -- replicant plugs stay unbound, subsystem
|
|
// criticals are master-authoritative). A NULL roster slot is left
|
|
// unbound: slot 0 (the control mapper) installs post-ctor and
|
|
// Link::AddToPlug derefs the plug.
|
|
//
|
|
if (criticalSubsystemCount > 0)
|
|
{
|
|
criticalSubsystems =
|
|
new MechCriticalSubsystem *[criticalSubsystemCount];
|
|
Register_Pointer(criticalSubsystems);
|
|
for (int cc = 0; cc < criticalSubsystemCount; ++cc)
|
|
{
|
|
criticalSubsystems[cc] = new MechCriticalSubsystem(this);
|
|
Register_Object(criticalSubsystems[cc]);
|
|
|
|
int subsystem_index = -1;
|
|
*stream >> criticalSubsystems[cc]->criticalWeight;
|
|
*stream >> criticalSubsystems[cc]->damagePercentage;
|
|
*stream >> subsystem_index;
|
|
criticalWeightSum += criticalSubsystems[cc]->criticalWeight;
|
|
|
|
if (
|
|
mech->GetInstance() == Entity::MasterInstance
|
|
&& mech->IsDynamic()
|
|
&& subsystem_index >= 0
|
|
&& subsystem_index < mech->GetSubsystemCount()
|
|
)
|
|
{
|
|
Subsystem *target = mech->GetSubsystem(subsystem_index);
|
|
if (target != NULL)
|
|
{
|
|
criticalSubsystems[cc]->subsystemPlug.Add(target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// The LOD redirect table: an ARTIFACT zone (low-LOD hull shell) lists
|
|
// the real child zones a hit on it lands on.
|
|
//
|
|
int redirect_count = 0;
|
|
*stream >> redirect_count;
|
|
{
|
|
for (int rr = 0; rr < redirect_count; ++rr)
|
|
{
|
|
int child_zone = 0;
|
|
*stream >> child_zone;
|
|
IntegerPlug *plug = new IntegerPlug(child_zone);
|
|
Register_Object(plug);
|
|
redirectTable.AddValue(plug, child_zone);
|
|
}
|
|
}
|
|
lastHitZone = (redirect_count == 0) ? 0 : RandomRedirect();
|
|
|
|
if (getenv("BT_SKEL_DUMP"))
|
|
{
|
|
DEBUG_STREAM << "[zone] " << damage_zone_index
|
|
<< " '" << damageZoneName << "'"
|
|
<< " pts=" << defaultArmorPoints
|
|
<< " seg=" << segmentIndex
|
|
<< " crit=" << criticalSubsystemCount
|
|
<< " redirect=" << redirect_count
|
|
<< (vitalDamageZone ? " VITAL" : "")
|
|
<< ((leftLeg || rightLeg) ? " LEG" : "")
|
|
<< (descendOnDestruction ? " DESCEND" : "")
|
|
<< (destroySiblingsOnDestruction ? " SIBS" : "")
|
|
<< endl << flush;
|
|
}
|
|
|
|
Check_Fpu();
|
|
}
|
|
|
|
Mech__DamageZone::~Mech__DamageZone()
|
|
{
|
|
if (criticalSubsystems != NULL)
|
|
{
|
|
for (int cc = 0; cc < criticalSubsystemCount; ++cc)
|
|
{
|
|
Unregister_Object(criticalSubsystems[cc]);
|
|
delete criticalSubsystems[cc];
|
|
}
|
|
Unregister_Pointer(criticalSubsystems);
|
|
delete [] criticalSubsystems;
|
|
}
|
|
|
|
DamageZoneIndexTableIterator iterator(redirectTable);
|
|
iterator.DeletePlugs();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// SetLODParentPointers (binary @0049d1d0). Run by the Mech ctor after EVERY
|
|
// zone of the mech exists: each redirect child's parentArtifactZone slot
|
|
// points back at this artifact zone (the parent-level damage recompute walks
|
|
// it -- cascade wave).
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech__DamageZone::SetLODParentPointers()
|
|
{
|
|
Check(this);
|
|
|
|
Mech *mech = (Mech *)GetOwningSimulation();
|
|
Check(mech);
|
|
|
|
DamageZoneIndexTableIterator iterator(redirectTable);
|
|
IntegerPlug *plug;
|
|
while ((plug = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
int child = *plug;
|
|
if (child >= 0 && child < mech->damageZoneCount)
|
|
{
|
|
Mech__DamageZone *child_zone =
|
|
(Mech__DamageZone *)mech->damageZones[child];
|
|
child_zone->parentArtifactZone.Add(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// TakeDamage (binary @0049c690, vtable slot 6) -- the full zone cascade.
|
|
//
|
|
// ARTIFACT zones (non-empty redirect table = the low-LOD hull shell) never
|
|
// take damage themselves: the hit is routed to a real child zone. REAL
|
|
// zones apply the armor model, refresh their artifact parent's displayed
|
|
// level, roll the special-damage generator short, and drive the mech's
|
|
// graphic/failure state: a destroyed non-leg zone recurses the destruction
|
|
// down the segment table; a destroyed VITAL zone (or leg) is the mech kill.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech__DamageZone::TakeDamage(Damage &damage)
|
|
{
|
|
Check(this);
|
|
|
|
Mech *mech = (Mech *)GetOwningSimulation();
|
|
Check(mech);
|
|
|
|
{
|
|
DamageZoneIndexTableIterator redirects(redirectTable);
|
|
if (redirects.GetSize() != 0)
|
|
{
|
|
//
|
|
// Artifact shell: hand the hit to a real child (same-attacker
|
|
// clustering lives in the router).
|
|
//
|
|
Damage routed = damage;
|
|
LODDamageRouter(mech->lastInflictingID, routed);
|
|
return;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Real zone: the engine armor model (amount x scale[type], clamp,
|
|
// Burning/Destroyed at 1.0).
|
|
//
|
|
Scalar preLevel = damageLevel;
|
|
DamageZone::TakeDamage(damage);
|
|
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[dmghit] zone=" << damageZoneIndex
|
|
<< " '" << damageZoneName << "'"
|
|
<< " type=" << (int)damage.damageType
|
|
<< " amt=" << damage.damageAmount
|
|
<< " lvl " << preLevel << "->" << damageLevel
|
|
<< (vitalDamageZone ? " VITAL" : "")
|
|
<< endl << flush;
|
|
}
|
|
|
|
Mech__DamageZone *parent = parentArtifactZone.GetCurrent();
|
|
if (parent != NULL)
|
|
{
|
|
parent->UpdateLODDamageLevel();
|
|
}
|
|
|
|
//
|
|
// Special damage: an Energy (type 4) hit on a zone with critical
|
|
// subsystems rolls ONE of them; a generator pick shorts it (the
|
|
// screen-flicker / weapons-dead moment; novice cockpits are exempt
|
|
// inside ForceShortRecovery).
|
|
//
|
|
if (damage.damageType == Damage::EnergyDamageType
|
|
&& criticalSubsystemCount > 0)
|
|
{
|
|
int pick = Random(criticalSubsystemCount);
|
|
Subsystem *victim = criticalSubsystems[pick]->subsystemPlug.GetCurrent();
|
|
if (victim != NULL
|
|
&& victim->IsDerivedFrom(Generator::ClassDerivations))
|
|
{
|
|
Generator *generator = (Generator *)victim;
|
|
if (!generator->NoviceLockout())
|
|
{
|
|
generator->ForceShort();
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[short] '" << generator->GetName()
|
|
<< "' SHORTED by Energy hit on zone "
|
|
<< damageZoneIndex << endl << flush;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Graphic / failure state from the new structure level. The leg
|
|
// branches are gated in the binary by the LIVE MovementMode (3||4 =
|
|
// driving; IsDisabled = 2||9 wreck) -- our gait FSM is a later wave, so
|
|
// the STAGED gate is "mech not already destroyed" (equivalent while the
|
|
// only wreck path is the death level itself).
|
|
//
|
|
if (vitalDamageZone == 0 || damageLevel < StructureMax)
|
|
{
|
|
if (leftLeg == 0 && rightLeg == 0)
|
|
{
|
|
if (damageLevel >= StructureMax)
|
|
{
|
|
RecurseSegmentTable(mech);
|
|
}
|
|
}
|
|
else if (damageLevel >= StructureMax)
|
|
{
|
|
// leg destroyed -> fall / mech kill
|
|
mech->statusAlarm.SetLevel(9);
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[death] LEG zone " << damageZoneIndex
|
|
<< " destroyed -- mech down" << endl << flush;
|
|
}
|
|
}
|
|
else if (!mech->IsMechDestroyed())
|
|
{
|
|
// half-gone leg -> the gimp-gait graphic (right 4, left 3)
|
|
if (rightLeg != 0 && damageLevel >= LegHalfStructure)
|
|
{
|
|
mech->statusAlarm.SetLevel(4);
|
|
}
|
|
else if (leftLeg != 0 && damageLevel >= LegHalfStructure)
|
|
{
|
|
mech->statusAlarm.SetLevel(3);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Vital zone destroyed -> mech death.
|
|
//
|
|
SetDamageZoneState(BurningState);
|
|
mech->statusAlarm.SetLevel(9);
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[death] VITAL zone " << damageZoneIndex
|
|
<< " '" << damageZoneName << "' destroyed -- MECH KILL"
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
|
|
if (damageLevel < StructureMin)
|
|
{
|
|
damageLevel = StructureMin;
|
|
}
|
|
if (damageLevel > StructureMax)
|
|
{
|
|
damageLevel = StructureMax;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// LODDamageRouter (binary @0049c40c): pick which real child of this artifact
|
|
// takes the hit. The same attacker hitting again within [0.25s, 10s] tends
|
|
// to land on the SAME child (damage clustering, hysteresis 0.33); a new
|
|
// attacker or a stale window re-rolls.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech__DamageZone::LODDamageRouter(EntityID inflicting, Damage damage)
|
|
{
|
|
Check(this);
|
|
|
|
int zone;
|
|
if (!(inflicting == lastInflicting))
|
|
{
|
|
zone = RandomRedirect();
|
|
}
|
|
else
|
|
{
|
|
Scalar dt = (Scalar)Now() - lastDamageTime;
|
|
if (dt < LODReuseWindowMin)
|
|
{
|
|
zone = lastHitZone;
|
|
}
|
|
else if (dt > LODReuseWindowMax)
|
|
{
|
|
zone = RandomRedirect();
|
|
}
|
|
else if ((Scalar)Random <= LODReuseHysteresis)
|
|
{
|
|
zone = lastHitZone;
|
|
}
|
|
else
|
|
{
|
|
zone = RandomRedirect();
|
|
}
|
|
}
|
|
|
|
lastHitZone = zone;
|
|
lastDamageTime = (Scalar)Now();
|
|
lastInflicting = inflicting;
|
|
|
|
Mech *mech = (Mech *)GetOwningSimulation();
|
|
if (zone >= 0 && zone < mech->damageZoneCount)
|
|
{
|
|
((Mech__DamageZone *)mech->damageZones[zone])->TakeDamage(damage);
|
|
}
|
|
UpdateLODDamageLevel();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// RandomRedirect (binary @0049c600): uniform pick over the redirect table.
|
|
//#############################################################################
|
|
//
|
|
int
|
|
Mech__DamageZone::RandomRedirect()
|
|
{
|
|
Check(this);
|
|
|
|
DamageZoneIndexTableIterator iterator(redirectTable);
|
|
int count = iterator.GetSize();
|
|
if (count <= 0)
|
|
{
|
|
return NullDamageZone;
|
|
}
|
|
IntegerPlug *plug = iterator.GetNth(Random(count));
|
|
return (plug != NULL) ? (int)*plug : NullDamageZone;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// UpdateLODDamageLevel (binary @0049c51c): an artifact's displayed level is
|
|
// the mean of its real children's levels.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech__DamageZone::UpdateLODDamageLevel()
|
|
{
|
|
Check(this);
|
|
|
|
Mech *mech = (Mech *)GetOwningSimulation();
|
|
int count = 0;
|
|
Scalar sum = 0.0f;
|
|
|
|
DamageZoneIndexTableIterator iterator(redirectTable);
|
|
IntegerPlug *plug;
|
|
while ((plug = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
int child = *plug;
|
|
if (child >= 0 && child < mech->damageZoneCount)
|
|
{
|
|
sum += ((Mech__DamageZone *)mech->damageZones[child])->damageLevel;
|
|
++count;
|
|
}
|
|
}
|
|
|
|
damageLevel = (count != 0) ? sum / (Scalar)count : StructureMin;
|
|
if (damageLevel < StructureMin)
|
|
{
|
|
damageLevel = StructureMin;
|
|
}
|
|
if (damageLevel > StructureMax)
|
|
{
|
|
damageLevel = StructureMax;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// CriticalHit (binary @0049ccc4): half the damage (capped 1.0) is carved off
|
|
// as the critical bite; the rest runs the normal armor model. The bite
|
|
// rolls ONE critical subsystem weighted by criticalWeight and applies the
|
|
// damage to it, charged against the entry's damagePercentage allotment.
|
|
// Returns the chosen subsystem (NULL if none).
|
|
//#############################################################################
|
|
//
|
|
Subsystem*
|
|
Mech__DamageZone::CriticalHit(Damage &damage_data)
|
|
{
|
|
Check(this);
|
|
|
|
Scalar bite = damage_data.damageAmount * CriticalDamageFraction;
|
|
if (bite > CriticalDamageMax)
|
|
{
|
|
bite = CriticalDamageMax;
|
|
}
|
|
damage_data.damageAmount -= bite;
|
|
|
|
TakeDamage(damage_data);
|
|
|
|
Scalar roll = (Scalar)Random;
|
|
if (criticalSubsystems != NULL && criticalWeightSum > 0.0f)
|
|
{
|
|
for (int ii = 0; ii < criticalSubsystemCount; ++ii)
|
|
{
|
|
Scalar cumulative = criticalSubsystems[ii]->criticalWeight;
|
|
for (int jj = 0; jj < ii; ++jj)
|
|
{
|
|
cumulative += criticalSubsystems[jj]->criticalWeight;
|
|
}
|
|
|
|
if (roll <= cumulative / criticalWeightSum)
|
|
{
|
|
Subsystem *victim =
|
|
criticalSubsystems[ii]->subsystemPlug.GetCurrent();
|
|
MechCriticalSubsystem *entry = criticalSubsystems[ii];
|
|
if (victim != NULL
|
|
&& entry->damagePercentageUsed < entry->damagePercentage)
|
|
{
|
|
entry->damagePercentageUsed +=
|
|
((MechSubsystem *)victim)->
|
|
ApplyDamageAndMeasure(damage_data);
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[crit] zone " << damageZoneIndex
|
|
<< " -> '" << victim->GetName()
|
|
<< "' used=" << entry->damagePercentageUsed
|
|
<< "/" << entry->damagePercentage
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
return victim;
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// SendSubsystemDamage (binary @0049c9a8): the zone is destroyed -- pin its
|
|
// structure, then push each critical entry's UNUSED damage allotment into
|
|
// the bound subsystem's own zone; a subsystem driven to 1.0 fails (and a
|
|
// VITAL one kills the mech).
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech__DamageZone::SendSubsystemDamage()
|
|
{
|
|
Check(this);
|
|
|
|
SetDamageZoneState(BurningState);
|
|
damageLevel = StructureMax;
|
|
|
|
for (int ii = 0; ii < criticalSubsystemCount; ++ii)
|
|
{
|
|
MechCriticalSubsystem *entry = criticalSubsystems[ii];
|
|
MechSubsystem *victim =
|
|
(MechSubsystem *)entry->subsystemPlug.GetCurrent();
|
|
if (victim == NULL)
|
|
{
|
|
// Unbound plug = roster slot 0/1 residue (mapper installs
|
|
// post-ctor / voltage-bus stub) -- the 1995 binary derefs
|
|
// unguarded because every plug there is bound.
|
|
continue;
|
|
}
|
|
|
|
Scalar level = victim->GetSubsystemDamageLevel()
|
|
+ (entry->damagePercentage - entry->damagePercentageUsed);
|
|
if (level < StructureMin)
|
|
{
|
|
level = StructureMin;
|
|
}
|
|
if (level > StructureMax)
|
|
{
|
|
level = StructureMax;
|
|
}
|
|
victim->SetSubsystemDamageLevel(level);
|
|
|
|
if (level >= StructureMax)
|
|
{
|
|
if (victim->IsVitalSubsystem())
|
|
{
|
|
((Mech *)GetOwningSimulation())->statusAlarm.SetLevel(9);
|
|
}
|
|
victim->ForceCriticalFailure();
|
|
if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[critfail] '" << victim->GetName()
|
|
<< "' DESTROYED (vital=" << victim->IsVitalSubsystem()
|
|
<< ") by zone " << damageZoneIndex << " death"
|
|
<< endl << flush;
|
|
}
|
|
}
|
|
else if (getenv("BT_MECH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[critfail] '" << victim->GetName()
|
|
<< "' damaged -> " << level << endl << flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// RecurseSegmentTable (binary @0049cad4): destruction descent. Push this
|
|
// zone's subsystem damage out, then -- per the streamed flags -- destroy the
|
|
// sibling zones on the same segment and/or descend into the child segments'
|
|
// zones, recursing on each.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech__DamageZone::RecurseSegmentTable(Mech *my_mech)
|
|
{
|
|
Check(this);
|
|
Check(my_mech);
|
|
|
|
SendSubsystemDamage();
|
|
SetGraphicState(DestroyedGraphicState);
|
|
|
|
EntitySegment *segment = my_mech->GetSegment(segmentIndex);
|
|
if (segment == NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (destroySiblingsOnDestruction)
|
|
{
|
|
EntitySegment::IntegerTableIterator *siblings =
|
|
segment->MakeDamageZoneIndexTable();
|
|
EntitySegment::IntegerPlug *plug;
|
|
while ((plug = siblings->ReadAndNext()) != NULL)
|
|
{
|
|
int zi = *plug;
|
|
if (zi >= 0 && zi < my_mech->damageZoneCount)
|
|
{
|
|
Mech__DamageZone *sibling =
|
|
(Mech__DamageZone *)my_mech->damageZones[zi];
|
|
if (sibling != this
|
|
&& sibling->GetGraphicState() != DestroyedGraphicState)
|
|
{
|
|
sibling->RecurseSegmentTable(my_mech);
|
|
sibling->SetGraphicState(GoneGraphicState);
|
|
}
|
|
}
|
|
}
|
|
delete siblings;
|
|
}
|
|
|
|
if (descendOnDestruction)
|
|
{
|
|
EntitySegment::IntegerTableIterator *children =
|
|
segment->MakeChildIndexTable();
|
|
EntitySegment::IntegerPlug *plug;
|
|
while ((plug = children->ReadAndNext()) != NULL)
|
|
{
|
|
EntitySegment *child = my_mech->GetSegment(*plug);
|
|
if (child == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
EntitySegment::IntegerTableIterator *child_zones =
|
|
child->MakeDamageZoneIndexTable();
|
|
EntitySegment::IntegerPlug *zone_plug;
|
|
while ((zone_plug = child_zones->ReadAndNext()) != NULL)
|
|
{
|
|
int zi = *zone_plug;
|
|
if (zi >= 0 && zi < my_mech->damageZoneCount)
|
|
{
|
|
Mech__DamageZone *child_zone =
|
|
(Mech__DamageZone *)my_mech->damageZones[zi];
|
|
// (defensive vs authored-cycle data; the 1995 binary
|
|
// trusted the tree and recursed unconditionally)
|
|
if (child_zone->GetGraphicState() != DestroyedGraphicState)
|
|
{
|
|
child_zone->RecurseSegmentTable(my_mech);
|
|
child_zone->SetGraphicState(GoneGraphicState);
|
|
}
|
|
}
|
|
}
|
|
delete child_zones;
|
|
}
|
|
delete children;
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// Graphic state. STAGED pass-through: the authentic override also flips the
|
|
// zone's skeleton-segment video objects to the destroyed skins (cascade
|
|
// wave); the base keeps the state machine + changed flags correct for the
|
|
// network update records.
|
|
//#############################################################################
|
|
//
|
|
void
|
|
Mech__DamageZone::SetGraphicState(Enumeration new_state)
|
|
{
|
|
Check(this);
|
|
DamageZone::SetGraphicState(new_state);
|
|
}
|