//===========================================================================// // 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 #pragma hdrstop #if !defined(MECHDMG_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(MECHSUB_HPP) # include #endif #if !defined(POWERSUB_HPP) # include #endif #if !defined(SEGMENT_HPP) # include #endif #if !defined(RANDOM_HPP) # include #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); }