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>
140 lines
8.0 KiB
Markdown
140 lines
8.0 KiB
Markdown
# MECHDMG.CPP — Mech::DamageZone (hull zone armor economy + destruction cascade)
|
||
|
||
**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
|
||
|
||
`CODE/BT/BT/MECHDMG.HPP` (06/05/95 JM) **survives in the archive** and wins the
|
||
include path (`$C/BT` precedes `$S/BT`), so this TU compiles against the real
|
||
1995 interface: `MechCriticalSubsystem` with a `SlotOf<Subsystem*>` plug and
|
||
**Scalar** criticalWeight, `Logical` zone flags, `TableOf<PlugOf<int>*,int>`
|
||
redirectTable, `SlotOf<Mech__DamageZone*> parentArtifactZone`. Only the .CPP is
|
||
a reconstruction. (A from-scratch staged header was written and then DELETED
|
||
the same session once the survivor was found — do not resurrect it.)
|
||
|
||
## Why this TU exists at wave 5.3.16 (the bring-up crash)
|
||
|
||
`Entity::Entity` (engine, ENTITY.CPP:1032) only reads the zone COUNT from the
|
||
type-0x14 DamageZoneStream and allocates the raw `damageZones[count]` POINTER
|
||
array — **it never constructs the zones**. The derived entity must fill the
|
||
slots itself (CulturalIcon = the surviving engine reference, CULTURAL.CPP:349;
|
||
the Mech = binary Pass 3, FUN_004a1674). Two crashes taught this:
|
||
|
||
1. Unfilled slots are heap garbage → the FIRST TakeDamageMessage dispatched at
|
||
a mech called through a trash vtable (exception 0E, EIP inside the heap).
|
||
2. Filling with the engine BASE `::DamageZone` streamed ctor parses each zone
|
||
record SHORT (the BT tail below goes unread) → the stream skews → the next
|
||
zone's effect-site indices read garbage → `GetSegment()` NULL →
|
||
`Link::AddToPlug(NULL)` (exception 0E at AddToPlug+9). The class-scope
|
||
`DamageZone` typedef (= `Mech__DamageZone`) in the authentic mech ctor is
|
||
the load-bearing signal: mech zones are ALWAYS the subclass.
|
||
|
||
## The per-zone stream record (type 0x14, after the count word)
|
||
|
||
Engine base part (DAMAGE.CPP:234, authentic order, VERIFIED identical RP↔BT411):
|
||
`CString name` · per 5 effect-site classes `{int n; n×int segIdx}` (JointedMover
|
||
owners resolve them via GetSegment; non-jointed read 5 filler ints) ·
|
||
`Scalar defaultArmorPoints` · `Scalar damageScale[5]` · material lists.
|
||
|
||
BT mech tail (this ctor, BT411-verified @0049ce50):
|
||
`Logical descend, destroySiblings` · `int segmentIndex` · `Logical leftLeg,
|
||
rightLeg, vital` · `int critCount` · critCount × `{Scalar weight; Scalar
|
||
damagePct; int rosterIndex}` · `int redirectCount` · redirectCount × `int
|
||
childZone`.
|
||
|
||
## The armor economy (BT411 combat-damage.md, binary-verified)
|
||
|
||
- Stream authors armor POINTS; ctor normalizes in place:
|
||
`scale[type] = 1/(raw[type]×armorPoints)` (cells already `1/armorPoints`
|
||
pass through, tolerance 1e-4); leg zones halve every type's scale.
|
||
- Base `DamageZone::TakeDamage` then applies
|
||
`damageLevel += amount × scale[type]`, clamp [0,1], 1.0 = Burning/Destroyed.
|
||
- `damageType` indexes the 5 cells DIRECTLY: 0 Collision / 1 Ballistic /
|
||
2 Explosive / 3 Laser / 4 Energy. **burstCount is NOT in the formula** —
|
||
one message = one application (it only feeds gyro bounce + splash falloff).
|
||
- Live-verified 5.3.16 (two-mech fight): PPC 11.77/hit type 4 (authored 12 ×
|
||
chargeRatio²), ER-M laser 3.43 type 3, SRM 5.83×6 type 2; repeat hits climb
|
||
linearly; 70-pt leg zones absorb at the halved scale; zones reach 1.0.
|
||
|
||
## Crit table binding
|
||
|
||
Streamed roster index → `subsystemPlug.Add(GetSubsystem(idx))`, gated
|
||
MasterInstance + DynamicFlag (binary @0049d0e1 — replicant plugs stay unbound,
|
||
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.
|
||
|
||
## The cascade (5.3.17, all binary-addressed via BT411, fight-verified)
|
||
|
||
- **`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.
|