Files
TeslaRel410/restoration/source410/BT/DAMAGE-MODEL.md
T
CydandClaude Fable 5 8231bee58c BT410 5.3.68: THE LIVE-RIO FAULT IS FIXED -- it and the TM crash were one defect
The [map] audit named it in one run: subsystem 17 = Torso, attribute ids 12/13
unresolved.  The donor's decompiled torso.hpp carries the authentic enum with
binary offsets -- ids 3..15, including StickPosition(9), TorsoUp/Down/
Left/Right(10-13), TorsoCenter(14), MotionState(15).  Our table stopped at
seven entries with MotionState at id 9, on the strength of a comment claiming
the rest were messages.

The streamed control mappings bind BY ID as direct WRITE destinations, so the
truncation produced two different crashes from one cause: in TM mode the
button ids 12/13 resolved NULL (the boot write-fault the moment the joystick
polled), and in RIO mode the analog id 9 resolved to our motionState
StateIndicator -- every analog packet from a live vRIO wrote raw floats over a
watcher-socketed object, and the corrupted chains walked into unmapped memory
~30-40s later.  That was the 'intermittent' pod fault at the fixed DPMI-host
address.  vRIO down = no analog = no corruption, which is why the norio conf
launched: every observation from the whole hunt drops out of this mechanism.

Fix: the full 13-entry donor table; five int command members carved from the
dynamicsState reserve (class size unchanged); ctor zeros them.

Verified, two agreeing runs each way: TM mode launches with a clean audit and
the Thrustmaster joystick driving the torso (stickY=0.907 -> torsoElev 0.349);
RIO mode with vRIO STREAMING launches, zero faults, weapons cycling.
pod_render_rec is no longer poisoned and the norio workaround is obsolete.

The audit-before-the-archive-call pattern (BT_MAP_LOG walks the same streamed
table CreateStreamedMappings consumes, naming what will not resolve) turned a
two-day intermittent-corruption hunt into a one-run lookup.  The streamed RES
tables are binding contracts on our attribute enums.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 14:41:52 -05:00

9.8 KiB
Raw Blame History

BT 4.10 — the damage model, end to end

How a shot becomes lost armor, dead equipment, and a mech kill. Synthesized from the surviving 1995 engine source (CODE/RP/MUNGA/), the reconstructed BT-side TUs (restoration/source410/BT/), and the BT411 binary reversal. Per-TU depth lives in the sibling *.NOTES.md files; this doc is the cross-cutting flow.

weapon fire ──► Damage record ──► TakeDamageMessage ──► victim handler
                                                          │ (unaimed? cylinder table)
                                                          ▼
                                              damageZones[zone]->TakeDamage
                                                          │
                        armor economy: level += amount × scale[type]
                                                          │
              ┌──────────────────┬────────────────────────┤
              ▼                  ▼                        ▼
        level ≥ 1.0        Energy special           level < 1.0
        zone destroyed     (generator short)        (leg ≥ 0.5 → limp)
              │
   ┌──────────┼──────────────┐
   ▼          ▼              ▼
 VITAL      LEG           other zone
 mech kill  mech down     SendSubsystemDamage (crit allotments → equipment)
                          + RecurseSegmentTable (SIBS / DESCEND)

1. The Damage record (engine: MUNGA/DAMAGE.HPP)

Every hit travels as one Damage struct:

field meaning
damageType 0 Collision · 1 Ballistic · 2 Explosive · 3 Laser · 4 Energy
damageAmount points (the only field in the armor formula)
damageForce impulse vector (physics/feel, not armor)
surfaceNormal, impactPoint world-space impact geometry
burstCount "times to apply" — NOT in the armor formula; one message = one application. Feeds splash falloff and the gyro bounce only.

2. Producers — where Damage records are born

Beam weapons (EMITTER.CPP, PPC/lasers): instant-hit at the owner's current target when rangeToTarget <= effectiveRange. The discharge energy splits by the authored ratio damageFraction = dmg/(dmg+heat); the delivered amount scales with charge: damagePortion = authored × chargeRatio² (an undercharged PPC hits soft). The heat portion goes into the FIRER's own heat sinks. Delivery = MechWeapon::SendDamage.

Ballistic / missile weapons (PROJWEAP.CPP, MISLANCH.CPP, MISSILE.CPP): the weapon FSM's Loaded case pulls ammo (FeedAmmo), checks jam, then FireWeapon spawns ONE cluster Missile entity per salvo (salvo-split damageAmount, burstCount = missile count). The round flies guided (seeker + thruster); the proximity fuse delivers the whole record once, impactPoint = the round's world position, zone = 1 (unaimed).

Explosions / splash (MUNGA/EXPLODE.CPP): a boxed splash volume gathers movers + cultural objects sorted by distance; each target gets burstCount = original / r^1.2 (min 1), force along the radius vector, zone = 1.

Collisions (MUNGA/MOVER.CPP ProcessCollisionList): type Collision, amount computed by the bounce resolver (StaticBounce from velocity, elasticity, friction), impact point from the collision slice. Multiple same-frame collisions are averaged, damage summed.

3. Delivery — Entity::TakeDamageMessage (ENTITY3.HPP)

Fields: inflictingEntity, damageZone (+invalidDamageZone = zone < 0), damageData, inflictingSubsystemID (so the BT message manager can bundle explosion resource IDs). Dispatched AT the victim entity.

The authored contract (ENTITY3.HPP warning): only reticle-based (aimed) weapons carry a valid zone. Everything else — missiles, splash, rams — arrives zone = 1 and must be resolved by the victim.

The attacker also posts a ScoreInflicted message to its own player per delivered hit (the damage score).

4. Victim routing — Mech::TakeDamageMessageHandler (MECH.CPP)

Binary hub @004a0230, in order:

  1. Feed the RAW record to the gyro (cockpit bounce — even an invalid-zone hit shakes the pilot). (staged: feel wave)
  2. Latch lastInflictingID — keys the LOD damage-clustering below.
  3. Unaimed resolve: if invalidDamageZone, map impactPoint through the cylinder hit-location table (DMGTABLE.CPP, type-29 resource, cached mech+0x444): world → mech-local; local height picks a ROW (feet rows are chassis-fixed, upper rows add the LIVE torso twist to the impact angle); atan2(z,x) picks the angular CELL; a uniform roll walks the cell's cumulative distribution (the BattleTech dice scatter) → hull zone.
  4. Chain to Entity::TakeDamageMessageHandler: zone 1 is DROPPED (base contract), otherwise damageZones[zone]->TakeDamage(damageData).

5. The hull zone — Mech__DamageZone::TakeDamage (MECHDMG.CPP)

Artifact (LOD) zones — a zone with a non-empty redirect table is a low-LOD hull shell: it never takes damage itself, it routes to a real child zone. Same-attacker clustering: within 0.25 s the SAME child is hit again; past 10 s re-roll; in between, 33 % reuse. The artifact's displayed level = mean of its children.

Real zones — the armor economy:

damageLevel += damageAmount × damageScale[damageType]      clamp [0,1]

Authoring streams armor POINTS per type; the ctor normalizes scale[type] = 1/(points[type] × armorPoints), so 1.0 = the zone's full point budget spent. Leg zones halve every scale (legs effectively carry double points). 1.0 → BurningState + DestroyedGraphicState.

Per-hit specials: an Energy hit on a zone with critical subsystems rolls ONE of them; if the pick is a Generator it is force-shorted (screens flicker, weapons drop dead until recovery; novice cockpits exempt).

State outcomes by the new level:

condition result
VITAL zone hits 1.0 mech kill (statusAlarm 9)
leg zone hits 1.0 fall → mech kill
leg zone ≥ 0.5 limp gait graphic (left 3 / right 4)
non-leg, non-vital hits 1.0 destruction descent (below)

6. Criticals and equipment — MECHDMG.CPP + MECHSUB.CPP

Each hull zone streams a critical table: {weight, damagePercentage allotment, roster subsystem index} per entry (plug binding is master-authoritative — replicants never bind).

Every subsystem owns a PRIVATE DamageZone (index 0, not in the hull array) with its own points + per-type scales (structureReference + armorByFacing[5], same normalization rule). That's what makes criticals measurable.

  • CriticalHit (aimed/critical fire; not yet on the weapon path — reticle wave): HALF the damage (cap 1.0) is carved off as the critical bite, applied to ONE weight-rolled crit subsystem via ApplyDamageAndMeasure, charged against that entry's allotment; the remainder runs the normal zone armor model.
  • Zone death → SendSubsystemDamage: pins the zone at 1.0 and pushes each entry's UNUSED allotment into its subsystem's private zone. A subsystem at 1.0 → ForceCriticalFailure: alarm level 1 (Destroyed), SetSimulationState(DestroyedState) — the hard gate every weapon FSM polls, so destroyed weapons fall silent; a VITAL subsystem kills the mech. Repeat hits on a dead zone re-run the push (binary-authentic) — contained equipment keeps degrading under continued fire.

7. The destruction cascade — RecurseSegmentTable

A destroyed zone walks the skeleton by its streamed flags:

  • SIBS (destroySiblingsOnDestruction): destroy the other zones on the same segment.
  • DESCEND (descendOnDestruction): destroy every zone on the child segments, recursing.

Fight-verified chain: arm zone dies → SIBS kills the searchlight zone (its Searchlight/ThermalSight crits destroyed) → DESCEND kills the gun zone → PPC_2 + ERMLaser_2 + Condenser6 destroyed and STOP FIRING, while the untouched PPC_1 keeps shooting.

8. Data authoring (the resource chain)

  • Type-20 DamageZoneStream (per mech): zone count, then per zone the engine record (name, 5 effect-site segment lists, defaultArmorPoints, damageScale[5], material lists) + the BT tail (descend/sibs flags, segmentIndex, leftLeg/rightLeg/vital, crit table, LOD redirect table). Authored in .dmg notation files: WeaponDamagePoints default with Collision/Ballistic/Explosive/Laser/EnergyDamagePoints overrides (stored as 1/points).
  • Type-29 DamageLookupTableStream: the cylinder table (rows × angular cells × cumulative zone distributions; 18 tables shipped).
  • Weapon subsystem resources: damageAmount, damageType, heatCostToFire, range, recharge.
  • Reference magnitudes (bhk1): legs 70 pts, upper torso 124, searchlights 25; PPC ~12 (Energy), ER-M laser 3.43 (Laser), SRM 5.83 × 6 (Explosive).

9. Replication and respawn

  • Criticals resolve on the MASTER instance only; DamageZone state replicates via update records (damageLevel, zone state, graphic state, changed flags). SubsystemMessageManager consolidates per-frame damage and bundles explosion resources (not yet reconstructed).
  • Mech::Reset (respawn) heals every hull zone and DeathResets the roster, but crit damagePercentageUsed PERSISTS across lives — spent crit budgets stay spent (binary-authentic).

10. Known deltas from the 1995 binary (staged)

  • Aimed fire: beam SendDamage currently rolls a uniform random hull zone; authentic = the reticle hit. CriticalHit is unwired for the same reason.
  • Cylinder height = 10.0 constant (binary reads the collision cylinder).
  • Leg-branch gates use "not already destroyed" instead of the live MovementMode/IsDisabled checks (gait FSM pending).
  • Gyro hit-feed and destroyed-skin graphics are log stubs (feel/render waves).