--- id: combat-damage title: "Combat & Damage — targeting, firing, damage zones, death" status: established source_sections: "PROGRESS_LOG.md §10c; docs/HARD_PROBLEMS.md (P5)" related_topics: [subsystems, decomp-reference, reconstruction-gotchas, locomotion, rendering] key_terms: [damage-zone, TakeDamage, viewpoint-entity, master, replicant] open_questions: - "Per-impact aim (cylinder lookup); collision damage application; DeathShutdown + collapse anim" --- # Combat & Damage The drive→target→fire→damage→destroy loop. Full detail: `docs/PROGRESS_LOG.md §10c`; the death/ teardown forensics: `docs/HARD_PROBLEMS.md` (P5). ## No AI — BT is PvP-only (verified, not a recovery gap) The mission system is NETWORKED and BT never had NPCs/AI. Every mech is driven by a `BTPlayer` (or a `BTCameraDirector` spectator). The BT registry knows only Mech/Projectile/Missile/BTPlayer/BTTeam; no Bot/Brain/AI class exists; `BTL4OPT.EXE` has no AI strings. `Crusher/Blocker/Runner` are RED PLANET classes, NOT BT. ⇒ a solo mission has no enemy by design; `BT_SPAWN_ENEMY` is a TEST DUMMY (uncontrolled Mech). The authentic opponent path is [[multiplayer]]. [T1] ## Entity spawn + the per-mech gate `Mech::Make(MakeMessage*)` (`FUN_004a2d48`) allocs `sizeof(Mech)` + runs the ctor; the base Entity ctor self-registers it (renders + ticks). `Mech::PerformAndWatch` runs for EVERY mech → gate player-only logic on `this == application->GetViewpointEntity()` (else a spawned enemy is driven by player input). [T1] ## Targeting (mech offsets, [[decomp-reference]] §3) `mech+0x37c` = target world Point3D; `mech+0x388` = target `Entity*` (the `HasActiveTarget()` gate); `mech+0x38c` = targeted sub-zone (−1=whole). Weapons cache hasTarget/targetPoint/muzzlePoint, refreshed each frame. [T1] ## Firing arc + muzzles There is NO firing-arc field in the binary (`Emitter::FireWeapon` fires whenever HasActiveTarget) — the 1995 game got away with it because it was **cockpit-only** (a beam behind you is off-camera). The port's external camera exposes it, so the arc is gated on the **torso twist range** (`Torso::GetHorizontalReach`, 0 for the twist-disabled Blackhawk) + a base aim tolerance (`BT_FIRE_ARC`, a labeled PORT presentation param). Beams fire from the mech's real gun-port SITE segments (siterugunport etc.) resolved via `GetSegment(name)→segmentToEntity×localToWorld`. [T2] ## Weapon fire is a PORT reconstruction (the dpl_* beam layer is unported) `Emitter::FireWeapon` builds a muzzle→target beam but the `dpl_*` beam renderable was never ported → the beam is drawn by `BTPushBeam`/`BTDrawBeams` (L4VIDEO — an additive quad / the real `ermlaser.bgf` tube with the scrolling `bexp` grit). Fire rate is decoded-exact (DischargeTime 0.2 + RechargeRate 2.2 per ER-M laser; [[decomp-reference]] §5). Flying projectiles (LRM/autocannon) are also a PORT reconstruction (`BTPushProjectile` — the 2007 Entity is too small for the binary's raw integrator offsets). [T2] ## Damage delivery + the real damage model `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)` → `target->Dispatch`. **Base handler IGNORES zone==−1** (`Entity::TakeDamageMessageHandler`, ENTITY.cpp:878 — returns on `damageZone==−1`; the message carries `invalidDamageZone = damageZone<0` + `damageData.impactPoint`). The Mech override that resolves an unaimed hit's zone from the impact point (the **cylinder hit-location model**, the deferred **STEP 6**) is NOT reconstructed — meanwhile send a VALID zone (mech4 aims a vital zone). **Investigated 2026-07 (durable):** the earlier-cited addresses `FUN_004a0230`/`FUN_0049ed0c` do NOT exist — the real pieces are: the **CylinderDamageZoneTable** = a list of 0x30-byte cylinder entries (entry ctor `FUN_0049e740`) loaded from resource type **0x1d** by the mech's cylinder-table NAME; the table ctor is `FUN_0049ea48` (vtable 0x50bd84, alloc 0x2c, cached at `Mech[0x111]`=byte 0x444) — the recon DOES build it (mislabeled `StandingAnimation`, mech.cpp:1221) but with an EMPTY name → 0 entries → the lookup is a no-op. **Fully reversed (2026-07):** the table is a passive nested-list **height × angle grid** — 3 container classes: TABLE (0x2c, ctor `FUN_0049ea48` / dtor `0x49eadc`, vtable 0x50bd84, `this[3]`=mech, `this[4]`=row list, `this[10]`=row count) → ROWS by HEIGHT (0x30, ctor `FUN_0049e740` / dtor `0x49e814`, vtable 0x50bd90, `this[0xb]`=cell count, cell key `i·(2π/count)` — `_DAT_0049e810`=**6.2831855=2π**; `this[4]`=mech+0x438) → CELLS by ANGLE (0x2c, ctor `FUN_0049deb0` / dtor `0x49df80`, vtable 0x50bd9c) → zone ref(s). All three vtables are MINIMAL (dtor + 2 Node slots, no method) → **the lookup is entirely in the unexported `Mech::TakeDamageMessageHandler`** (a handler-SET entry, not a vtable slot — ENTITY.h has no `virtual`; find it via the Mech message-handler registration, then disassemble). So STEP 6 remaining = (1) fix the resource-name load (build the table WITH data, not the empty-name stub); (2) reconstruct the 3 container classes from the captured ctors; (3) disassemble + reconstruct `Mech::TakeDamageMessageHandler` (compute the impactPoint's local height+angle → index the grid → dispatch to `damageZones[zone]->TakeDamage`); (4) register the handler + verify unaimed hits distribute across zones. A genuine multi-pass reconstruction (the container classes are tedious; the handler is the disassembly). **Content confirmed (2026-07): the resource is type 29 = `DamageLookupTableStream`, 18 present in `BTL4.RES` (one per mech) — so STEP 6 is FEASIBLE, not content-blocked.** The recon's empty-name stub (mech.cpp:1219) must be fixed to the real name (the binary copies it from a sibling resource, `local_130+0xc` @part_012.c:10361 — reuse the mech's DamageZoneStream/model name). RE phase COMPLETE; the build (containers + handler + load + wire) is next. `Mech__DamageZone::TakeDamage` → engine armor model (`damageLevel += damageAmount*damageScale[type]`) → 1.0 = zone destroyed → death. The Mech ctor populates the inherited `damageZones[]` from the DamageZoneStream (type 0x14) resource. `class Damage{ damageType (Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. Verified: `structure` climbs, vital zone destroyed → `*** TARGET DESTROYED ***`. [T2] Two non-field fixes were needed: **message-handler chaining** (an unchained set drops TakeDamage silently) + **entity validity** (`SetValidFlag()` on a manually-spawned entity) — see [[reconstruction-gotchas]] §9. [T2] ## Death — the wreck STAYS (P5 CLOSED) A killed mech does NOT disappear — BT death is a STATE transition (`SetGraphicState(DestroyedGraphicState)` + death anim + effect/splash), the mech becomes a persistent WRECK (RP analog: `VTV::DeathShutdown`). **NEVER issue `DestroyEntityMessage` on death** — the teardown crash was an artifact of forcing a removal the original never does. The actual bug was ONE thing: an EXPLICIT `JointedMover::~JointedMover()` in the reconstructed `~Mech` ran the whole base-dtor chain TWICE (the dtor-epilogue rule, [[reconstruction-gotchas]] §7) → a double-free of `collisionLists` + the segment table. Removing it fixed BOTH the death-row crash AND app-exit crashes. [T2] ## Key Relationships - Weapons/roster: [[subsystems]]. Aim source: [[locomotion]] (drive/facing). Effects: [[rendering]]. - P5 forensics: `docs/HARD_PROBLEMS.md`. Data: [[decomp-reference]] §4-5.