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

198 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).