Files
TeslaRel410/restoration/source410/BT/DAMAGE-MODEL.md
T
CydandClaude Fable 5 33fd921cea BT410 5.3.89: the hit-location cylinder MEASURED -- 18 tables, 8 distinct, and two chassis that never twist
The operator recalled the damage model as "a pie wedged cylinder" and asked
how it maps across the mechs.  It is exactly that, and the shipped data is now
extracted rather than described.

dmgscan.py brute-forces every offset in BTL4.RES and accepts a candidate only
if the ENTIRE nested type-29 structure parses -- thresholds strictly ascending
and terminating at exactly 1.0, zone indices in range, names NUL-terminated.
A wrong format guess cannot survive that, so finding exactly 18 tables -- the
count DAMAGE-MODEL.md already claimed from an independent reversal -- is a
confirmation of the format, not a coincidence.

MEASURED: 18 tables, every one 7 bands x 8 wedges = 56 cells.  Only EIGHT are
distinct by content; the other ten are duplicates.

  22 zones  4 twisting  x3  Avatar / Mad Cat class -- table A
  22 zones  4 twisting  x2  Avatar / Mad Cat class -- table B
  21 zones  4 twisting  x3  Loki
  22 zones  4 twisting  x2  Thor
  21 zones  4 twisting  x2  SND2
  24 zones  4 twisting  x2  Battlemaster / Vulture
  20 zones  0 twisting  x2  Black Hawk
  17 zones  0 twisting  x2  Owens

BLACK HAWK AND OWENS ROTATE NO BAND WITH THE TORSO.  Every other chassis
rotates its upper four.  That is a real behavioural difference in the shipped
data, not an absence of it.

The zone COUNTS match the per-chassis .SKL dz_ sets exactly, which is what
lets a table be fingerprinted back to a chassis.  It is not always unique --
Avatar and Mad Cat share a zone set but have two DIFFERENT tables, and no
chassis name sits near the stream, so they are recorded A/B rather than
guessed.  Stated as undetermined in both the notes and the visual.

THE GEOMETRY, now named: 18 wedge names in six anatomical rings (Foot, Leg,
Hip, Waist, Chest, Top).  Slot 0 starts at angle 0 spanning 45 degrees, so
under atan2(z,x) the mech's +X is right and +Z is front.  Each named face
covers TWO adjacent wedges (Right = 7,0 / Front = 1,2 / Left = 3,4 / Rear =
5,6) -- so dead ahead is the SEAM between two Front cells, never the centre of
one.  Bands 0-2 are chassis-fixed; the live torso twist is added to the impact
angle before the wedge pick on the rest.

And the scatter is generous in a way worth knowing at the controls: a clean
foot-wedge hit is only 50% that foot, 30% the lower leg, and 20% of the time
the OTHER foot entirely.

ALSO: an interactive plate of all of it -- every cell of all 8 tables, plan and
elevation, and a twist slider that rotates the upper bands live -- published
for the playtesters.  Its dataset regenerates from dmgscan.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 23:10:41 -05:00

13 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).

11. The cylinder table, measured — dmgscan.py

Section 4.3 describes the unaimed resolve; this is the shipped data behind it, extracted by restoration/dmgscan.py (brute-forces every offset and accepts only candidates whose entire nested structure parses, so the format itself is under test). 18 tables, every one 7 bands x 8 wedges = 56 cells.

Only 8 are distinct by content — the other 10 are duplicates.

distinct table zones torso-rotating bands copies chassis family (by zone-set fingerprint)
b586e6f9 22 4 3 Avatar / Mad Cat class — table A
bdf3d3a8 22 4 2 Avatar / Mad Cat class — table B
ad4eb428 21 4 3 Loki
db6cc00d 22 4 2 Thor
2b34fe3e 21 4 2 SND2
9b481293 24 4 2 Battlemaster / Vulture
11c840bc 20 0 2 Black Hawk
4c07e216 17 0 2 Owens

The zone COUNTS (17/20/21/22/24) match the per-chassis .SKL dz_ sets exactly, which is what allows the fingerprint. It is not always unique: the Avatar and Mad Cat class share a zone set but have two DIFFERENT tables, and the resource carries no chassis name near the stream, so which is which is undetermined — recorded as A/B rather than guessed.

Black Hawk and Owens rotate NO band with the torso. Everything else rotates its upper four. That is a real behavioural difference, not missing data.

The 18 wedge names — six anatomical rings

band 6  TopRight   TopLeft                              (2 names / 8 slots)
band 5  RightChest FrontChest LeftChest RearChest       (4)
band 4  RightWaist FrontWaist LeftWaist RearWaist       (4)
band 3  RightHip   FrontHip   LeftHip   RearHip         (4)
band 2  RightLeg   FrontHip   LeftLeg   RearHip         (transition)
band 1  RightLeg   LeftLeg                              (2)
band 0  RightFoot  LeftFoot                             (2)

Slot 0 starts at angle 0 and each spans 45 degrees, so with atan2(z,x) the mech's +X is right and +Z is front. The named faces each cover TWO adjacent wedges (Right = slots 7,0 · Front = 1,2 · Left = 3,4 · Rear = 5,6), which means dead ahead is the seam between two Front cells, not the centre of one.

Sample cell distributions (Avatar/Mad Cat table A)

band 0  RightFoot   50% rfoot · 30% rdleg · 20% lfoot
band 1  RightLeg    40% ruleg · 40% rdleg · 10% luleg · 10% ldleg
band 5  FrontChest  50% utorso · 10% each larm/rarm/ltorso/rtorso/dtorso

Note the scatter is deliberate and generous: a clean foot hit is only half a foot hit, and one shot in five crosses to the OTHER foot.

Playtester-facing visual

An interactive plate of all of the above — every cell of all 8 tables, a plan and elevation, and a torso-twist slider that rotates the upper bands live — was published for playtesting. Regenerate its dataset with dmgscan.py.