Reconstructs the mech DEATH state machine — the un-exported master-perf death
branch (region 0x4a9770-0x4ab188) — from its EXPORTED consumers + the RP
VTV::DeathShutdown analog, wired into the active path (the bring-up drive
override bypasses the authentic Simulate where this normally lives).
Mech::UpdateDeathState() + IsMechDestroyed() (mech4.cpp), called for every mech
early in PerformAndWatch: on a vital kill (graphicAlarm >= 9, raised by the
damage side) it
1. sets movementMode = 5 -> the collapse clip's one-shot latch in
AdvanceBodyAnimation (fall direction 5-8 is un-exported -> 5 [T3]);
2. loops the roster calling Subsystem::DeathShutdown(1) (RP VTV::DeathShutdown
analog; the base is a no-op virtual, overrides act -- a SHUTDOWN not a
teardown, so it frees nothing and never removes the entity: the wreck STAYS);
3. next frame settles to movementMode = 9 -> IsDisabled -> locomotion frozen.
The drive's lone `movementMode = 1` write (mech4.cpp ~1309) is guarded on
!IsMechDestroyed so the death state is not clobbered back to a live gait.
Runtime-verified (forced kill, BT_DEATH_LOG=1):
[death] mech destroyed -> collapse + subsystem shutdown (wreck stays)
[death] mech settled -> disabled (IsDisabled=1, frozen wreck)
mech frozen in place, subsystem tick + renderer keep running, no crash, wreck
stays.
Also records the full death-sequence decomp map in combat-damage.md (the
exported consumers: AdvanceLeg/BodyAnimation collapse latch, IsDestroyed, the
MechDeathHandler effect engine; and the un-exported orchestration gap).
Deferred (honest): the visible collapse ANIMATION latch through the active path
(gait SM shows state=0 post-death; spawned mechs don't advance body anim);
MechDeathHandler (FUN_0042a984/FUN_0042aa2c -- the exported per-subsystem
destroyed-skin + explosion engine, still a stub) for death explosions; the
whole-mech DeathSplash radius damage (un-exported).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Mech per-impact hit-location resolver (the cylinder damage table) is now
functional, wired, and runtime-verified [T2]. Unaimed (zone==-1) hits — the
collision-damage path — now resolve an impact point to a damage zone via the
authentic height x angle grid + weighted dice roll, instead of dropping.
dmgtable.cpp/.hpp was a non-functional skeleton on no-op ReconTable/stream
shims; backed it with real std::vector storage and fixed 5 latent runtime bugs:
- ReadEntries now consumes the leading cell name-string ([i32 len][len+1])
- PieSlice ctor reads rotateWithTorso into the correct member
- SelectSlice direct-indexes (was int lookup on a float-keyed table)
- ResolveHit returns the zone (chains SelectSlice -> SelectZone)
- real MemoryStream::ReadBytes (was a variadic no-op)
mech.cpp ctor: replaced the empty-name StandingAnimation stub with the real
load — FindResourceDescription(dzRes->resourceName, type 0x1d) -> stream ->
new DamageLookupTable, cached at mech[0x111]; ~Mech deletes it.
Mech::TakeDamageMessageHandler override registered (MESSAGE_ENTRY overlays
Entity's by ID): on invalidDamageZone, resolve via the table then base-route;
aimed reticle hits pass through unchanged.
Three named accessors (no databinding-trap raw reads): WorldToLocal
(localToWorld.MultiplyByInverse), CylinderReferenceHeight (standingTemplateMaxY
== collisionTemplate->maxY == binary mech+0x2ec[+0xc]), TorsoHeading via a
BTGetTorsoTwist bridge in torso.cpp (Torso::CurrentTwist == torso+0x1d8;
torso.hpp cannot be included into mech.cpp — subsystem-stub collision).
Stream format + geometry + roll + handler were all byte-verified against the
shipped BTL4.RES type-29 resources (18 tables, exact consumption) and the
disassembly (FUN_0049eb54/e678/de14, glue 0x49ed0c, handler @0x4a037a).
Runtime: boots clean, "[cyl] table 'bhk1' layers=7" (exact byte-verified layer
count, found by name), mech spawns + walks, no asserts/AV/0xCDCDCDCD. Env gate
BT_CYL_LOG=1. Unblocks collision-damage application.
KB updated (combat-damage.md STEP 6 COMPLETE, open-questions.md marked done).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cylinder table resource = type 29 DamageLookupTableStream, 18 in BTL4.RES (one per mech) ->
STEP 6 is FEASIBLE, not content-blocked. Name copied from a sibling resource (local_130+0xc).
RE phase complete; build (containers + handler + load + wire) is the next phase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>