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>
198 lines
9.8 KiB
Markdown
198 lines
9.8 KiB
Markdown
# 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).
|