The binding was in the zone ctor all along: Ghidra dropped the two arg pushes @0049d0e1 (Slot::AddImplementation(subsystemArray[streamedIndex])), making it read as a bare Resolve(). DZSlot stand-in -> engine SlotOf<T>; SendSubsystemDamage rewritten to the recovered @0049c9a8 body (allotment into the subsystem's OWN private zone; vital -> graphicAlarm 9); CriticalHit -> real ApplyDamageAndMeasure; parentArtifactZone.Add revived (LOD damage averaging); videoObjectFlag renamed vitalSubsystem (+0xE4). BT_CRIT_PROBE diag added. Verified: 66 plugs bound/mech; probe-destroyed zone -> crits damaged/DESTROYED, statusAlarm + destroyed-skin chain fire; MP kill + solo un-regressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1094 lines
43 KiB
C++
1094 lines
43 KiB
C++
//==========================================================================//
|
|
// 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 Slot::GetCurrentPlug (READ; was mislabeled Resolve)
|
|
// FUN_00417a80 Slot::AddImplementation (BIND -- Ghidra dropped its args)
|
|
// 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
|
|
parentArtifactZone(0) // FUN_0049ddc9(,0) -- Socket(NULL) legal
|
|
{
|
|
// 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)
|
|
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); // roster index (mech+0x128 order)
|
|
|
|
// THE BINDING (task #2, disasm @0049d0e1-0049d10d -- Ghidra DROPPED
|
|
// the two arg pushes, which is why this read as a bare Resolve():
|
|
// the real call is Slot::AddImplementation(roster[index])). Gate =
|
|
// MasterInstance + DynamicFlag on the flags LWord @+0x28
|
|
// (simulationFlags). Replicant plugs stay UNBOUND on purpose:
|
|
// subsystem criticals are master-authoritative. [T1]
|
|
if ((mech->simulationFlags & Entity::InstanceMask) == Entity::MasterInstance
|
|
&& (mech->simulationFlags & Entity::DynamicFlag) != 0)
|
|
{
|
|
Subsystem *target =
|
|
(subsystemIndex >= 0 && subsystemIndex < mech->GetSubsystemCount())
|
|
? mech->GetSubsystem(subsystemIndex) : 0;
|
|
if (target != 0)
|
|
{
|
|
criticalSubsystems[i]->subsystemPlug.Add(target); // FUN_00417a80
|
|
if (getenv("BT_DEATH_LOG"))
|
|
DEBUG_STREAM << "[crit] zone " << damage_zone_index
|
|
<< " entry " << i << " -> roster[" << subsystemIndex
|
|
<< "] = " << (void *)target << "\n" << std::flush;
|
|
}
|
|
else if (getenv("BT_DEATH_LOG"))
|
|
{
|
|
// roster slot NULL: idx 0 (mapper, installed post-ctor) or
|
|
// idx 1 (voltage bus, ReconSlot stub). The engine
|
|
// Link::AddToPlug derefs the plug, so a null bind crashes.
|
|
DEBUG_STREAM << "[crit] zone " << damage_zone_index
|
|
<< " entry " << i << " roster[" << subsystemIndex
|
|
<< "] is NULL -- left unbound\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// 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.Add(this); // slot @0x188: FUN_00417a80 (NOT flag-gated)
|
|
}
|
|
}
|
|
|
|
//#############################################################################
|
|
// 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.GetCurrent(); // 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.GetCurrent();
|
|
if (s != 0 // unbound-plug guard (idx-0/1 roster residue)
|
|
&& 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.GetCurrent();
|
|
MechCriticalSubsystem *cs = criticalSubsystems[i];
|
|
if (s != 0 // unbound-plug guard
|
|
&& cs->damagePercentageUsed < cs->damagePercentage)
|
|
cs->damagePercentageUsed +=
|
|
((MechSubsystem *)s)->ApplyDamageAndMeasure(damage_data); // FUN_004ac07c
|
|
return s;
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
MechSubsystem *s =
|
|
(MechSubsystem *)criticalSubsystems[i]->subsystemPlug.GetCurrent(); // +4
|
|
MechCriticalSubsystem *cs = criticalSubsystems[i];
|
|
|
|
// Unbound-plug guard: with the authentic binding live (the ctor loop)
|
|
// this is only the idx-0/1 roster residue (mapper installed post-ctor
|
|
// / voltage-bus stub), not the norm. The 1995 binary derefs with no
|
|
// check because every plug there is bound.
|
|
if (s == 0)
|
|
{
|
|
if (getenv("BT_DEATH_LOG"))
|
|
DEBUG_STREAM << "[deathfx] crit-subsys " << i << "/" << criticalSubsystemCount
|
|
<< " UNBOUND (roster slot residue) -- skipped\n" << std::flush;
|
|
continue;
|
|
}
|
|
|
|
// Push the UNUSED crit allotment into the subsystem's OWN private
|
|
// DamageZone (the 0x160-byte zone its ctor built at +0xE0) -- NOT a
|
|
// mech dz_* zone (the old SubProxy2 cast misread this). [T1 @0049c9a8]
|
|
Scalar level = s->GetSubsystemDamageLevel()
|
|
+ (cs->damagePercentage - cs->damagePercentageUsed);
|
|
level = Clamp(level, StructureMin, StructureMax);
|
|
s->SetSubsystemDamageLevel(level);
|
|
|
|
if (level >= StructureMax) // subsystem destroyed
|
|
{
|
|
if (s->IsVitalSubsystem()) // +0xE4
|
|
((Mech *)GetOwningSimulation())->graphicAlarm.SetLevel(9); // mech kill
|
|
s->ForceCriticalFailure(); // status 1 + print + zone valve
|
|
if (getenv("BT_DEATH_LOG"))
|
|
DEBUG_STREAM << "[deathfx] crit-subsys " << i
|
|
<< " DESTROYED (vital=" << s->IsVitalSubsystem() << ")\n" << std::flush;
|
|
}
|
|
else if (getenv("BT_DEATH_LOG"))
|
|
{
|
|
DEBUG_STREAM << "[deathfx] crit-subsys " << i << " damaged -> "
|
|
<< level << "\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
//#############################################################################
|
|
// 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
|
|
}
|
|
|
|
void
|
|
Mech__DamageZone::Heal()
|
|
{
|
|
// Respawn heal: restore this zone to the undamaged/intact state the loader
|
|
// builds -- full structure, intact skin, no burning/destroyed flag. The
|
|
// authentic Mech::Reset (@0049fb74) heals via each subsystem's
|
|
// ResetToInitialState; the zone's structural state is restored here.
|
|
damageLevel = StructureMin; // 0.0 -- full structure (this+0x158)
|
|
ApplyDamageGraphicState(ExistsGraphicState); // 0 -- intact skin (undo Destroyed/Gone)
|
|
SetDamageZoneState(0); // clear burning / vital-destroyed state
|
|
}
|
|
|
|
//#############################################################################
|
|
// 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), wasWrecked(0)
|
|
{
|
|
// 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];
|
|
|
|
// FALLING edge = the master's Mech::Reset healed + re-broadcast this zone
|
|
// (ForceUpdate(DamageZoneUpdateModelFlag)). The rising-damage swap below is
|
|
// one-way; on a REPLICANT observer, reverse it here -- drop the destroyed
|
|
// segment meshes (BTRebuildMechModel) and play the recovery warp -- so the
|
|
// peer's wreck un-wrecks + warps back on the observer's screen. Gated to
|
|
// ReplicantInstance: the master already un-wrecks + warps locally in
|
|
// Mech::Reset (mech4.cpp), so it must NOT double-fire here. The authentic
|
|
// engine drives this off the type-0 graphic-state transition in
|
|
// Mech::ReadUpdateRecord (mech.cpp case 0 -> ResetPose); our port has no
|
|
// ported RemakeEntity state, so we react to the replicated zone level.
|
|
if (level < prev)
|
|
{
|
|
lastLevel[i] = level;
|
|
if (wasWrecked && prev >= 1.0f && level < 1.0f)
|
|
{
|
|
wasWrecked = 0; // one un-wreck per death->respawn
|
|
if (owner->GetInstance() == Entity::ReplicantInstance)
|
|
{
|
|
extern void BTRebuildMechModel(Entity *entity);
|
|
extern void BTStartWarpEffect(float x, float y, float z);
|
|
BTRebuildMechModel((Entity *)owner);
|
|
BTStartWarpEffect((float)owner->localOrigin.linearPosition.x,
|
|
(float)owner->localOrigin.linearPosition.y,
|
|
(float)owner->localOrigin.linearPosition.z);
|
|
if (getenv("BT_DEATH_LOG"))
|
|
DEBUG_STREAM << "[respawn] replicant un-wrecked + warp at ("
|
|
<< owner->localOrigin.linearPosition.x << ","
|
|
<< owner->localOrigin.linearPosition.z << ")\n" << std::flush;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (level <= prev) // unchanged -- no new damage
|
|
continue;
|
|
lastLevel[i] = level;
|
|
|
|
const Mech__DamageZone::DamageDescriptor *d = zone->DescriptorForLevel(level);
|
|
if (level >= 1.0f && prev < 1.0f) // just destroyed -> the Destroyed descriptor
|
|
{
|
|
wasWrecked = 1; // latch: a wreck the observer must later reverse
|
|
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;
|
|
}
|
|
}
|
|
}
|