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