Files
BT411/context/combat-damage.md
T
arcattackandClaude Opus 4.8 970d4fbad0 context: STEP-6 cylinder table FULLY reversed (height x angle grid) — build plan
De-risk + format-reversal pass complete. The CylinderDamageZoneTable is a passive nested-list
height x angle grid: TABLE (FUN_0049ea48) -> ROWS-by-height (FUN_0049e740, cell key i*2pi/count,
_DAT_0049e810=6.2831855) -> CELLS-by-angle (FUN_0049deb0) -> zone. All 3 vtables minimal (dtor +
2 Node slots, NO lookup method) -> the lookup is entirely in the unexported
Mech::TakeDamageMessageHandler (handler-set entry, not virtual) = the disassembly target.
Full ctor/dtor/vtable address map + the 4-step build plan recorded in combat-damage.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 23:20:44 -05:00

6.9 KiB
Raw Blame History

id, title, status, source_sections, related_topics, key_terms, open_questions
id title status source_sections related_topics key_terms open_questions
combat-damage Combat & Damage — targeting, firing, damage zones, death established PROGRESS_LOG.md §10c; docs/HARD_PROBLEMS.md (P5)
subsystems
decomp-reference
reconstruction-gotchas
locomotion
rendering
damage-zone
TakeDamage
viewpoint-entity
master
replicant
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). 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