Per the 'correct errors' convention: the cited FUN_004a0230/FUN_0049ed0c don't exist. The real CylinderDamageZoneTable = a list of 0x30-byte entries (FUN_0049e740) from resource 0x1d; the recon builds the table (FUN_0049ea48, mislabeled StandingAnimation @Mech[0x111]) with an EMPTY name -> 0 entries -> no-op. The lookup + Mech::TakeDamageMessageHandler override are NOT in the exported decomp (need disassembly). Recorded in combat-damage + open-questions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
5.9 KiB
Markdown
86 lines
5.9 KiB
Markdown
---
|
||
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. The **lookup + the `Mech::TakeDamageMessageHandler` override are NOT in the exported
|
||
decomp** (assert-anchored pass skipped them → need disassembly). So STEP 6 = fix the resource name +
|
||
reverse the 0x30-byte entry format (`FUN_0049e740`) + disassemble & reconstruct the handler/lookup + wire
|
||
the vtable override. `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.
|