BT410 Phase 5.3.17: the destruction cascade -- zone deaths DESTROY things
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>
This commit is contained in:
@@ -352,6 +352,12 @@ void
|
||||
Mech *enemy = Mech::Make(&create_enemy);
|
||||
Register_Object(enemy);
|
||||
((Mech *)playerVehicle)->SetTargetEntity(enemy);
|
||||
//
|
||||
// Mutual: the enemy targets us back -- with BT_FORCE_FIRE its
|
||||
// weapons return fire (its gates read dev-permissive, no player
|
||||
// link), giving the damage cascade a live INCOMING side too.
|
||||
//
|
||||
enemy->SetTargetEntity(playerVehicle);
|
||||
|
||||
if (getenv("BT_MECH_LOG"))
|
||||
{
|
||||
|
||||
@@ -172,3 +172,10 @@ and sets it as the player mech's target. The enemy has NO player link, so its
|
||||
experience gates read dev-permissive; it never fires back (no target of its
|
||||
own — set one for a mutual fight). DEV-ONLY: the real opponent path is the
|
||||
network mesh / mission roster.
|
||||
|
||||
## 5.3.17 — BT_SPAWN_ENEMY is now a MUTUAL fight
|
||||
|
||||
The harness also sets the enemy's target back to the player: under
|
||||
BT_FORCE_FIRE the enemy returns fire (its gates read dev-permissive with no
|
||||
player link), so the damage cascade gets a live incoming side — used to
|
||||
verify the player's own weapons fall silent when destroyed.
|
||||
|
||||
@@ -64,10 +64,29 @@ Derivation
|
||||
"Mech"
|
||||
);
|
||||
|
||||
//
|
||||
// The Mech's OWN handler table, chained to the JointedMover set. The
|
||||
// TakeDamage entry OVERRIDES Entity's by message ID (Build overlays the
|
||||
// inherited slot -- no gap risk, the parent chain already registered the
|
||||
// ID); every other inherited message still routes to its base handler.
|
||||
//
|
||||
const Receiver::HandlerEntry
|
||||
Mech::MessageHandlerEntries[] =
|
||||
{
|
||||
MESSAGE_ENTRY(Mech, TakeDamage)
|
||||
};
|
||||
|
||||
Mech::MessageHandlerSet
|
||||
Mech::MessageHandlers(
|
||||
ELEMENTS(Mech::MessageHandlerEntries),
|
||||
Mech::MessageHandlerEntries,
|
||||
JointedMover::MessageHandlers
|
||||
);
|
||||
|
||||
Mech::SharedData
|
||||
Mech::DefaultData(
|
||||
Mech::ClassDerivations,
|
||||
JointedMover::MessageHandlers,
|
||||
Mech::MessageHandlers,
|
||||
JointedMover::AttributeIndex,
|
||||
33,
|
||||
(Entity::MakeHandler)Mech::Make
|
||||
@@ -167,6 +186,7 @@ Mech::Mech(
|
||||
lookPitch = 0.0f;
|
||||
lookYaw = 0.0f;
|
||||
targetEntity = NULL;
|
||||
lastInflictingID = EntityID::Null;
|
||||
|
||||
//
|
||||
// Look-view angles: defaults until the GameModel read below overrides them
|
||||
@@ -551,6 +571,41 @@ Mech::~Mech()
|
||||
{
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// TakeDamageMessageHandler -- the Mech override of the Entity damage entry
|
||||
// (binary hub @004a0230 via the handler glue @0049ed0c).
|
||||
//
|
||||
// Order in the binary: (1) feed the RAW Damage record to the gyro cockpit
|
||||
// bounce FIRST -- even an invalid-zone hit shakes the cockpit (STAGED: the
|
||||
// gyro bounce math is the gyro/feel wave; a gated log marks the feed site);
|
||||
// (2) latch the attacker (mech+0x43c -- the zone LOD router keys its
|
||||
// same-attacker redirect reuse off it); (3) resolve an unaimed hit's zone
|
||||
// (invalidDamageZone) from the cylinder hit-location table -- DEFERRED, the
|
||||
// type-0x1d table is not loaded yet, so unaimed hits fall through to the
|
||||
// base handler's zone==-1 drop (authentic base behavior); (4) chain to the
|
||||
// Entity handler which routes damageZones[zone]->TakeDamage.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
Mech::TakeDamageMessageHandler(TakeDamageMessage *message)
|
||||
{
|
||||
Check(this);
|
||||
Check(message);
|
||||
|
||||
if (gyroSubsystem != NULL && getenv("BT_MECH_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[gyro] hit feed staged (type="
|
||||
<< (int)message->damageData.damageType
|
||||
<< " amt=" << message->damageData.damageAmount << ")"
|
||||
<< endl << flush;
|
||||
}
|
||||
|
||||
lastInflictingID = message->inflictingEntity;
|
||||
|
||||
Entity::TakeDamageMessageHandler(message);
|
||||
}
|
||||
|
||||
void
|
||||
Mech::SetMappingSubsystem(Subsystem *subsystem)
|
||||
{
|
||||
|
||||
@@ -147,6 +147,12 @@
|
||||
class Mech:
|
||||
public JointedMover
|
||||
{
|
||||
//
|
||||
// The per-zone damage code walks the roster / segment table / alarms
|
||||
// directly (the 1995 arrangement, mirrored by the reconstruction).
|
||||
//
|
||||
friend class Mech__DamageZone;
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Shared Data Support
|
||||
//
|
||||
@@ -334,6 +340,22 @@
|
||||
void
|
||||
SetTargetEntity(Entity *target) { Check(this); targetEntity = target; }
|
||||
|
||||
//
|
||||
// Damage entry (overrides the Entity handler): latches the attacker
|
||||
// (mech+0x43c, read by the zone LOD router) and feeds the gyro
|
||||
// cockpit bounce before chaining to the base zone routing. The
|
||||
// cylinder unaimed-hit resolver (type-0x1d table) is a later wave.
|
||||
//
|
||||
void
|
||||
TakeDamageMessageHandler(TakeDamageMessage *message);
|
||||
|
||||
//
|
||||
// Damage-side death flag: the body graphic alarm (statusAlarm,
|
||||
// binary @0x714) at level >= 9 = the death/fall state.
|
||||
//
|
||||
Logical
|
||||
IsMechDestroyed() { Check(this); return statusAlarm.GetLevel() >= 9; }
|
||||
|
||||
//
|
||||
// Look-state commit (called by the controls mapper on a look-button
|
||||
// state change): re-aim the eyepoint from the model's authored look
|
||||
@@ -411,6 +433,8 @@
|
||||
Scalar lookPitch;
|
||||
Scalar lookYaw;
|
||||
Entity *targetEntity;
|
||||
EntityID lastInflictingID; // binary mech+0x43c -- last
|
||||
// attacker (zone LOD router)
|
||||
|
||||
//
|
||||
// The model's authored look-view angles (rad; deg in the resource).
|
||||
@@ -425,7 +449,7 @@
|
||||
// mech2/mech3/mech4 simulation. Reserved until that path is
|
||||
// reconstructed with named fields.
|
||||
//
|
||||
int reservedState[201];
|
||||
int reservedState[199];
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -380,3 +380,16 @@ Found while verifying the novice-mode heat lockout end-to-end.
|
||||
all zones (binary Pass-3 order, FUN_004a1674). Full forensics in
|
||||
MECHDMG.NOTES.md — the Entity ctor ONLY allocates the pointer array; an
|
||||
unfilled or base-class fill is the 5.3.16 crash pair.
|
||||
|
||||
## 5.3.17 — the Mech damage entry + friend
|
||||
|
||||
- `Mech::MessageHandlerEntries/MessageHandlers` now DEFINED (chained to
|
||||
JointedMover's set; Build overlays the TakeDamage slot by ID — no gap risk,
|
||||
the parent chain registered it). DefaultData switched onto it.
|
||||
- `TakeDamageMessageHandler` override (binary hub @004a0230): latches
|
||||
`lastInflictingID` (mech+0x43c, keys the zone LOD router's same-attacker
|
||||
clustering), gyro-bounce feed STAGED (log marker), cylinder unaimed-zone
|
||||
resolution DEFERRED (type-0x1d table), chains to Entity's handler.
|
||||
- `friend class Mech__DamageZone` (BT411 mech.hpp:301 — authentic): the
|
||||
per-zone damage code walks the roster / segment table / statusAlarm.
|
||||
- `IsMechDestroyed()` = statusAlarm (the @0x714 body-graphic alarm) >= 9.
|
||||
|
||||
@@ -28,9 +28,37 @@
|
||||
# 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
|
||||
@@ -197,6 +225,22 @@ Mech__DamageZone::Mech__DamageZone(
|
||||
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();
|
||||
}
|
||||
@@ -250,19 +294,469 @@ void
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Damage entry. STAGED: the engine base armor model applies directly --
|
||||
// the authentic override (@0049c690) first routes ARTIFACT hits through the
|
||||
// LOD redirect table to a real child zone, recomputes the artifact parent's
|
||||
// displayed level, rolls critical-subsystem hits and updates the leg-failure
|
||||
// state. All of that is the mechdmg cascade wave; the armor economy
|
||||
// (amount x normalized scale, clamp, Burning/Destroyed at 1.0) is live NOW.
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# MECHDMG.CPP — Mech::DamageZone (hull zone armor economy)
|
||||
# MECHDMG.CPP — Mech::DamageZone (hull zone armor economy + destruction cascade)
|
||||
|
||||
**Status: PARTIAL (5.3.16) — streamed ctor / LOD parent pass / dtor reconstructed; TakeDamage + SetGraphicState are staged base pass-throughs.**
|
||||
**Status: RECONSTRUCTED (5.3.17) — streamed ctor, TakeDamage cascade, LOD
|
||||
router, CriticalHit, SendSubsystemDamage, RecurseSegmentTable all live and
|
||||
fight-verified. Remaining staged bits listed at the bottom.**
|
||||
|
||||
## The header is AUTHENTIC
|
||||
|
||||
@@ -64,20 +66,74 @@ criticals are master-authoritative) AND NULL-guarded: roster slot 0 (control
|
||||
mapper) installs post-ctor, slot 1 (voltage bus) can be empty, and
|
||||
`Link::AddToPlug` derefs the plug.
|
||||
|
||||
## Staged / deferred (the mechdmg cascade wave)
|
||||
## The cascade (5.3.17, all binary-addressed via BT411, fight-verified)
|
||||
|
||||
- `TakeDamage` override = base pass-through. Authentic (@0049c690): artifact
|
||||
zones route hits through the redirect table to a real child
|
||||
(`LODDamageRouter`/`RandomRedirect`/`lastHitZone`), parent recomputes its
|
||||
displayed level (`UpdateLODDamageLevel`), critical rolls (`CriticalHit`),
|
||||
generator shorting, leg-failure state.
|
||||
- `SetGraphicState` override = base pass-through (authentic also flips the
|
||||
segment video objects to destroyed skins).
|
||||
- Declared-not-defined (unreferenced, so they link): `LODDamageRouter`,
|
||||
`RandomRedirect`, `UpdateLODDamageLevel`, `ShortAttachedGenerators`,
|
||||
`SendSubsystemDamage`, `RecurseSegmentTable`, `CriticalHit`,
|
||||
`CreateStreamedDamageZone`, `GetSegmentIndex`.
|
||||
- `lastHitZone` staged 0 (authentic seeds via RandomRedirect when redirects
|
||||
exist); `lastDamageTime` staged 0 (authentic = TheTime tick / TicksPerSecond).
|
||||
- The type-0x1e critical-descriptor stream + MechDeathHandler (BT411
|
||||
mech.cpp Pass-3 tail) are NOT loaded yet — same cascade wave.
|
||||
- **`TakeDamage` (@0049c690)**: artifact zones (redirect table non-empty)
|
||||
route via `LODDamageRouter`; real zones apply the engine armor model, then
|
||||
refresh their artifact parent (`UpdateLODDamageLevel` = mean of children),
|
||||
roll the special-damage short (Energy hit + a Generator crit pick →
|
||||
`Generator::ForceShort()`, novice-exempt), and drive failure state: a
|
||||
destroyed non-leg zone runs `RecurseSegmentTable`; a destroyed LEG or
|
||||
VITAL zone is the mech kill (statusAlarm level 9 = the @0x714 body-graphic
|
||||
death level; `Mech::IsMechDestroyed()`); half-gone legs set the gimp
|
||||
graphic (right 4 / left 3). The binary's MovementMode(3||4)/IsDisabled
|
||||
gates on the leg branches are STAGED as "not already destroyed" until the
|
||||
gait FSM exists.
|
||||
- **`LODDamageRouter` (@0049c40c)**: same-attacker clustering — reuse the
|
||||
previous child inside [0.25s, 10s] (hysteresis 0.33 — a binary DOUBLE,
|
||||
bytes 1f85eb51b81ed53f); new attacker or stale window re-rolls
|
||||
(`RandomRedirect` @0049c600, uniform over the redirect table). Keyed by
|
||||
`mech->lastInflictingID` (mech+0x43c), latched by the new
|
||||
`Mech::TakeDamageMessageHandler` override.
|
||||
- **`CriticalHit` (@0049ccc4)**: half the damage (cap 1.0) is the critical
|
||||
bite; the rest runs the armor model. Weighted roll over criticalWeight;
|
||||
the pick takes `ApplyDamageAndMeasure` charged against its
|
||||
damagePercentage allotment. (Not yet called by the weapon path — the
|
||||
aimed/critical fire distinction is the reticle wave.)
|
||||
- **`SendSubsystemDamage` (@0049c9a8)**: zone death pins structure 1.0 and
|
||||
pushes each crit entry's UNUSED allotment into the bound subsystem's own
|
||||
zone; 1.0 → `ForceCriticalFailure` (+ mech kill if vital). NOTE: repeat
|
||||
hits on a dead zone re-run the push (the binary is equally unguarded) —
|
||||
contained equipment keeps degrading under continued fire (HUD climbing
|
||||
0.33→0.67→0.99 in the verification run is this).
|
||||
- **`RecurseSegmentTable` (@0049cad4)**: destruction descent via the ENGINE
|
||||
EntitySegment API (`GetSegment` + `MakeDamageZoneIndexTable` /
|
||||
`MakeChildIndexTable` — heap iterators, caller deletes). SIBS = other
|
||||
zones on the same segment; DESCEND = child segments' zones; recursion
|
||||
guarded by GetGraphicState()!=Destroyed (authentic for siblings; added
|
||||
defensively on children).
|
||||
- **Weapon hard-failure gate**: `ForceCriticalFailure` now also sets
|
||||
`SetSimulationState(DestroyedState)` (MechSubsystem enum from the binary
|
||||
PrintState strings: Destroyed=1 @0050df7f, Exploding=2 @0050df8c) — the
|
||||
`GetSimulationState() == 1` check both weapon FSMs already carried goes
|
||||
live: destroyed weapons fall silent.
|
||||
|
||||
## Fight verification (bhk1 vs bhk1, BT_FORCE_ZONE=8 concentrated fire)
|
||||
|
||||
Zone topology (BT_SKEL_DUMP `[zone]` dump): NO artifact zones in this art
|
||||
(redirect=0 everywhere — matches the 1995 header comment); arms/guns carry
|
||||
DESCEND+SIBS; all torso + leg zones VITAL; legs 70pts (halved scale ✓),
|
||||
searchlights 25pts, utorso 124pts. Verified live: arm (zone 8) driven to 1.0
|
||||
→ SIBS killed the searchlight zone (Searchlight2 + ThermalSight crits
|
||||
destroyed, HUD degrading) → DESCEND killed the gun zone → **PPC_2 +
|
||||
ERMLaser_2 + Condenser6 DESTROYED and the weapons STOP FIRING** (0 shots
|
||||
after both mechs' PPC_2 died; undamaged PPC_1 kept firing); vital-zone kill
|
||||
([death] MECH KILL on dz_reardtorso) in the random-spread run; PPC Energy
|
||||
hit shorted GeneratorD through the crit plug binding. Dev hooks:
|
||||
BT_FORCE_ZONE=n (pin every SendDamage hit), BT_SPAWN_ENEMY now MUTUAL (the
|
||||
enemy returns fire under BT_FORCE_FIRE).
|
||||
|
||||
## Still staged / deferred
|
||||
|
||||
- `SetGraphicState` override = base pass-through (BT411 found the binary
|
||||
override byte-identical to the base; the destroyed-skin video flip is the
|
||||
render wave).
|
||||
- Declared-not-defined (unreferenced): `ShortAttachedGenerators` (its logic
|
||||
is inlined in TakeDamage per the binary), `UpdateLODDamageLevel` callers
|
||||
beyond TakeDamage, `CreateStreamedDamageZone`, `GetSegmentIndex` (tool
|
||||
side), `Heal`/death-reset family.
|
||||
- The type-0x1e critical-descriptor stream + MechDeathHandler (band
|
||||
descriptors, smoke/burn effects) — render/effects wave.
|
||||
- The cylinder hit-location table (type 0x1d) for unaimed zone -1 hits —
|
||||
the Mech handler currently lets the base drop them (authentic base
|
||||
behavior); reticle/aimed fire is the render wave.
|
||||
|
||||
@@ -119,8 +119,42 @@ MechSubsystem::MechSubsystem(
|
||||
configureActivePress = -1;
|
||||
statusAlarm.SetLevel(0);
|
||||
|
||||
damageZone = new DamageZone(owner, GetSegmentIndex());
|
||||
//
|
||||
// The subsystem's PRIVATE damage zone (binary @4abf28: `new
|
||||
// DamageZone(this, 0)` -- owned by the SUBSYSTEM, index 0, NOT an entry
|
||||
// in the entity's hull-zone array). The resource authors its armor the
|
||||
// same way the hull stream does -- structureReference points +
|
||||
// armorByFacing[5] per-type values -- normalized with the same rule as
|
||||
// Mech::DamageZone (scale = 1/(raw x points); already-normalized cells
|
||||
// pass through). This is what makes critical hits MEASURABLE
|
||||
// (ApplyDamageAndMeasure) and the crit-allotment push meaningful.
|
||||
//
|
||||
damageZone = new DamageZone(this, 0);
|
||||
Register_Object(damageZone);
|
||||
damageZone->damageZoneName = GetName();
|
||||
if (subsystem_resource->structureReference > 0.0f)
|
||||
{
|
||||
damageZone->defaultArmorPoints = subsystem_resource->structureReference;
|
||||
for (int tt = 0; tt < Damage::DamageTypeCount; ++tt)
|
||||
{
|
||||
Scalar raw = subsystem_resource->armorByFacing[tt];
|
||||
Scalar even = 1.0f / subsystem_resource->structureReference;
|
||||
Scalar diff = raw - even;
|
||||
if (diff < 0.0f)
|
||||
{
|
||||
diff = -diff;
|
||||
}
|
||||
if (diff > 1.0e-4f && raw > 0.0f)
|
||||
{
|
||||
damageZone->damageScale[tt] =
|
||||
1.0f / (raw * subsystem_resource->structureReference);
|
||||
}
|
||||
else
|
||||
{
|
||||
damageZone->damageScale[tt] = raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -181,12 +215,36 @@ void
|
||||
{
|
||||
Check(this);
|
||||
statusAlarm.SetLevel(MechSubsystem::Destroyed);
|
||||
//
|
||||
// DestroyedState is the hard-failure gate the weapon simulations poll
|
||||
// (`GetSimulationState() == 1`): a destroyed weapon drops its charge /
|
||||
// pins the unavailable alarm from the next frame on.
|
||||
//
|
||||
SetSimulationState(DestroyedState);
|
||||
if (damageZone)
|
||||
{
|
||||
damageZone->SetDamageZoneState(DamageZone::BurningState);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// ApplyDamageAndMeasure (binary @4ac07c): apply through the virtual
|
||||
// TakeDamage and report the zone-level delta -- the critical accountant
|
||||
// (Mech::DamageZone::CriticalHit) charges the delta against the entry's
|
||||
// damagePercentage allotment.
|
||||
//#############################################################################
|
||||
//
|
||||
Scalar
|
||||
MechSubsystem::ApplyDamageAndMeasure(Damage &damage)
|
||||
{
|
||||
Check(this);
|
||||
Scalar before = (damageZone != NULL) ? damageZone->damageLevel : 0.0f;
|
||||
TakeDamage(damage);
|
||||
Scalar after = (damageZone != NULL) ? damageZone->damageLevel : 0.0f;
|
||||
return after - before;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// TakeDamage Forward to the damage zone (per-type scaling happens there).
|
||||
|
||||
@@ -75,6 +75,18 @@
|
||||
TechStatusTypeCount
|
||||
};
|
||||
|
||||
//
|
||||
// The subsystem SIMULATION states (binary PrintState @4ac8c0 string
|
||||
// pool: "Destroyed" @0050df7f, "Exploding" @0050df8c). DestroyedState
|
||||
// (1) is the hard-failure gate every weapon simulation checks
|
||||
// (`GetSimulationState() == 1` -- emitter @4baab9, ballistic @4bbd36).
|
||||
//
|
||||
enum {
|
||||
DestroyedState = Simulation::DefaultState + 1,
|
||||
ExplodingState,
|
||||
MechSubsystemStateCount
|
||||
};
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Test Class Support
|
||||
//
|
||||
@@ -132,6 +144,18 @@
|
||||
void
|
||||
ForceCriticalFailure();
|
||||
|
||||
//
|
||||
// Critical-hit application (binary @4ac07c): run the damage through
|
||||
// TakeDamage and return how much the subsystem's own zone level
|
||||
// actually moved (the crit accountant charges it against the entry's
|
||||
// damagePercentage allotment).
|
||||
//
|
||||
Scalar
|
||||
ApplyDamageAndMeasure(Damage &damage);
|
||||
|
||||
Logical
|
||||
IsVitalSubsystem() const { Check(this); return vitalSubsystem; }
|
||||
|
||||
//
|
||||
// The NOVICE-experience lockout predicate (binary @4ac9c8): a novice
|
||||
// pilot's advanced cockpit controls (condenser valves, coolant flush,
|
||||
|
||||
@@ -173,3 +173,21 @@ Then the Mech member layout (subsystemArray/subsystemCount + the capability chai
|
||||
controllable/heatable/weaponRoster/damageable + the alarms/reticle/state-indicators
|
||||
/SequenceControllers/NameFilter/CStrings embedded members) must be named in
|
||||
MECH.HPP for the ctor to set them.
|
||||
|
||||
## 5.3.17 — the subsystem side of the damage cascade
|
||||
|
||||
- The subsystem's PRIVATE zone is now ARMED (binary @4abf28): owned by the
|
||||
SUBSYSTEM (`new DamageZone(this, 0)` — Joint.cpp:512 proves subsystems are
|
||||
valid zone owners), named, `defaultArmorPoints = structureReference`,
|
||||
`damageScale[5] = armorByFacing[]` normalized with the hull-zone rule
|
||||
(1/(raw×points); guard: structureReference<=0 leaves the zone inert).
|
||||
This makes crits MEASURABLE.
|
||||
- `ApplyDamageAndMeasure` (@4ac07c): TakeDamage + return the zone-level
|
||||
delta (the crit accountant charges it).
|
||||
- `ForceCriticalFailure` now also sets `SetSimulationState(DestroyedState)` —
|
||||
the subsystem SIMULATION state enum (Destroyed=1/Exploding=2, from the
|
||||
binary PrintState string pool) added to MECHSUB.HPP; both weapon FSMs'
|
||||
`GetSimulationState() == 1` hard gate is now live (destroyed weapons stop
|
||||
firing — fight-verified).
|
||||
- `IsVitalSubsystem()` accessor (vital crit death = mech kill in
|
||||
SendSubsystemDamage).
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#endif
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#if !defined(RANDOM_HPP)
|
||||
# include <random.hpp>
|
||||
@@ -325,6 +326,22 @@ void
|
||||
if (target->damageZoneCount > 0)
|
||||
{
|
||||
zone = Random(target->damageZoneCount);
|
||||
|
||||
//
|
||||
// DEV hook BT_FORCE_ZONE=n: pin every hit to zone n (concentrated
|
||||
// fire -- exercises the zone-death cascade headlessly).
|
||||
//
|
||||
{
|
||||
const char *force_zone = getenv("BT_FORCE_ZONE");
|
||||
if (force_zone != NULL)
|
||||
{
|
||||
int forced = atoi(force_zone);
|
||||
if (forced >= 0 && forced < target->damageZoneCount)
|
||||
{
|
||||
zone = forced;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Entity::TakeDamageMessage
|
||||
|
||||
@@ -212,6 +212,38 @@ Scalar
|
||||
return (stretched > voltageScale) ? stretched : voltageScale;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// ForceShortRecovery (binary @004b11bc): on a short event, drive the powering
|
||||
// generator to Shorted and clear its output; GeneratorSimulation then runs
|
||||
// the short-recovery timer back to Ready. Both @4ac9c8 calls in the binary
|
||||
// are the not-novice experience predicate (this part and its source share
|
||||
// the same mech, hence the same player) -- novice cockpits never see
|
||||
// electrical shorts.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
PoweredSubsystem::ForceShortRecovery()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
if (NoviceLockout())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Generator *source = (Generator *)voltageSource.Resolve();
|
||||
if (source != NULL)
|
||||
{
|
||||
source->ForceShort();
|
||||
if (getenv("BT_POWER_LOG") || getenv("BT_MECH_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[short] '" << source->GetName()
|
||||
<< "' SHORTED by special damage" << endl << flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// PoweredSubsystemSimulation -- the per-frame electrical step (binary
|
||||
|
||||
@@ -102,6 +102,17 @@
|
||||
Scalar
|
||||
ChargeTimeScale();
|
||||
|
||||
//
|
||||
// Short event (binary @004b11bc): drive the powering generator into
|
||||
// the Shorted state and zero its output -- the electrical FSM then
|
||||
// runs its short-recovery cycle. Gated on the not-novice experience
|
||||
// predicate (both hops share the mech's player). Called by the
|
||||
// special-damage path (a type-4 Energy hit on a zone with attached
|
||||
// generator criticals).
|
||||
//
|
||||
void
|
||||
ForceShortRecovery();
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Per-frame simulation (heat step + the electrical state machine).
|
||||
//
|
||||
@@ -269,6 +280,20 @@
|
||||
unsigned
|
||||
GeneratorStateOf() { Check(this); return stateAlarm.GetLevel(); }
|
||||
|
||||
//
|
||||
// The short event landing ON this generator (the tail of binary
|
||||
// @004b11bc): Shorted state + output killed; GeneratorSimulation
|
||||
// runs the short-recovery timer back to Ready. Callers gate on the
|
||||
// novice experience predicate.
|
||||
//
|
||||
void
|
||||
ForceShort()
|
||||
{
|
||||
Check(this);
|
||||
stateAlarm.SetLevel(GeneratorShorted);
|
||||
outputVoltage = 0.0f;
|
||||
}
|
||||
|
||||
//
|
||||
// Tap the generator for one load (a PoweredSubsystem attaching): -1 when
|
||||
// every tap is taken, else 0.
|
||||
|
||||
Reference in New Issue
Block a user