Collision damage applied: mech-vs-mech + icon-crunch via the STEP-6 unaimed path

The two deferred TakeDamage dispatches in Mech::ProcessCollision now fire,
unblocked by STEP 6 (the cylinder hit-location override that resolves zone==-1):

  - Mover branch (:15324-15358): on a collision with another Mech, dispatch the
    collision damage to it.
  - CulturalIcon branch (:15369-15401): crunch dispatch to a building/tree/prop,
    before the walk-through sentinel overwrites the amount.

Both go through a new file-scope helper BTDispatchCollisionDamage, which builds
an Entity::TakeDamageMessage{zone==-1} (the engine ctor -- same idiom as the
weapon-impact path) with the world centre of the overlap slice as the impact
point and this mech as the inflictor, then Dispatch()es it to the victim.  The
receiver turns the world impact point into a damage zone: a Mech via its cylinder
table (STEP 6), an icon via its base handler (crushable props have no zones -> a
harmless no-op).  Terrain (walls/hills) matches neither branch, so it still
BLOCKS without damage (faithful to the binary).

Faithful to the binary's raw DamageMessage dispatch (:15324-15401) but via the
engine's named TakeDamageMessage API (no databinding-trap field-by-field build);
the binary's inflictor global DAT_0050b9ac is a sentinel EntityID (only ever
read, never set -> a collision has no "shooter"), so this mech is the inflictor.

Verify: builds clean; reachability GUARANTEED (Mover::ProcessCollisionList calls
the VIRTUAL ProcessCollision -> Mech::ProcessCollision, on the active
AuthenticGroundAndCollide path); stable across runs; both mechs build their
cylinder tables.  The live dispatch was not captured headlessly (the solo
auto-walker never rammed a tree/mech), but the path is proven reachable and
composed of runtime-verified pieces (the weapon TakeDamage path + STEP 6).

Also validated STEP 6's height ref: collisionTemplate->maxY ~= 7.1 (a real mech
height), confirming CylinderReferenceHeight reads a height (not the heat value
the mech+0x2ec dual-labeling hinted at).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 08:54:21 -05:00
co-authored by Claude Opus 4.8
parent d07ac7dd49
commit 2af401eef8
2 changed files with 63 additions and 7 deletions
+11 -4
View File
@@ -71,10 +71,17 @@ authentic path scoped.
- Authentic per-mech TURN-RATE constant (currently a bring-up constant rate).
- Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand);
airborne callbacks (`FUN_004a6344`/`FUN_004a7970`).
- Wall-block-vs-climb tuning; collision DAMAGE application (computed, not applied). **UNBLOCKED 2026-07-08:**
STEP 6 (cylinder hit-location) now resolves an unaimed impact point → zone, so collision damage can be
applied through `Mech::TakeDamageMessageHandler` (a zone==1 hit now resolves instead of dropping). The
remaining piece is issuing the computed collision Damage into the TakeDamage path (currently computed only).
- Wall-block-vs-climb tuning. **✅ Collision DAMAGE application — DONE 2026-07-08 [T2].** The two deferred
dispatches in `Mech::ProcessCollision` (mech4.cpp: mech-vs-mech `:15324-15358` + icon-crunch `:15369-15401`)
now fire: on a collision with another Mech or a CulturalIcon, an `Entity::TakeDamageMessage{zone==1}` is
dispatched to the victim (via `BTDispatchCollisionDamage`, the engine ctor — same as the weapon path), and
STEP 6's cylinder table resolves the impact point → a zone on the receiver. Terrain (walls/hills) matches
neither branch → blocks without damage (faithful). Reachability is guaranteed (`Mover::ProcessCollisionList`
calls the **virtual** `ProcessCollision``Mech::ProcessCollision`); built + stable. The live dispatch
wasn't captured headlessly (the solo auto-walker never rammed a tree/mech), but the path is proven reachable
and composed of runtime-verified pieces. **Note:** validated STEP 6's height ref along the way —
`collisionTemplate->maxY ≈ 7.1` (a real mech height), confirming `CylinderReferenceHeight` reads a height,
not the heat value the `mech+0x2ec` dual-labeling (heat-gauge sink vs groundRef, mech4.cpp:2697) hinted at.
- **✅ Cylinder hit-location (STEP 6) — DONE 2026-07-08 [T2].** Built + runtime-verified: the `dmgtable.cpp`
classes now have real storage + a working `ResolveHit→zone`, the mech ctor loads the type-0x1d table by the
DamageZoneStream name (`[cyl] table 'bhk1' layers=7`), and `Mech::TakeDamageMessageHandler` resolves
+52 -3
View File
@@ -2600,6 +2600,45 @@ void
// baseline BT_COLLISION push-out path is byte-identical.
//###########################################################################
//###########################################################################
//
// Build a zone==-1 (unaimed) collision TakeDamageMessage from a resolved contact
// and dispatch it to 'victim'. The victim turns the WORLD impact point into a
// damage zone -- a Mech via its cylinder table (STEP 6), an icon via its base
// handler (crushable props have no zones -> the base handler no-ops). Faithful
// to the binary's mech-vs-mech / icon-crunch dispatches (:15324-15401): those
// built a raw DamageMessage{id=0x64, inflictor=DAT_0050b9ac, zone=-1}; we use the
// engine Entity::TakeDamageMessage ctor (as the weapon path does) with this mech
// as the inflictor. The impact point is the world centre of the overlap slice.
//
static void
BTDispatchCollisionDamage(
Mech *inflictor,
Entity *victim,
const Damage *resolved,
BoxedSolidCollision &collision)
{
Damage dmg;
dmg.damageType = Damage::CollisionDamageType;
dmg.damageAmount = resolved->damageAmount;
dmg.surfaceNormal = resolved->surfaceNormal;
dmg.impactPoint = Point3D(
(collision.collisionSlice.minX + collision.collisionSlice.maxX) * 0.5f,
(collision.collisionSlice.minY + collision.collisionSlice.maxY) * 0.5f,
(collision.collisionSlice.minZ + collision.collisionSlice.maxZ) * 0.5f);
dmg.burstCount = 1;
Entity::TakeDamageMessage take_damage(
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
inflictor->GetEntityID(), -1 /*unaimed -> receiver's cylinder resolves*/, dmg);
victim->Dispatch(&take_damage);
if (GroundLog())
DEBUG_STREAM << "[collide] dmg=" << resolved->damageAmount
<< " -> victim class=" << (int)victim->GetClassID()
<< " (zone==-1, resolved by receiver)\n" << std::flush;
}
void
Mech::ProcessCollision(
Scalar time_slice,
@@ -2660,8 +2699,14 @@ void
// that would silently defeat this gate).
if (damage->surfaceNormal * rel < -1.0e-4f) // _DAT_004ac044 (:15320-15323)
return;
// DEFERRED: mech-vs-mech TakeDamageMessage{0x64, len 0x12, zone=-1}
// dispatch to the other mech (:15324-15358) -- see the banner.
// Mech-vs-mech (:15324-15358): the binary gates on owner ClassID == Mech
// (0xbb9) then dispatches the collision DamageMessage (zone==-1) to the
// other mech. STEP 6 (its cylinder table) now resolves the zone, so this
// is UNBLOCKED (was deferred: zone==-1 used to be dropped).
if (owner->IsDerivedFrom(*Mech::GetClassDerivations()))
{
BTDispatchCollisionDamage(this, owner, damage, collision);
}
}
// --- CulturalIcon owner (buildings/trees/props) (:15361-15404) ----------
@@ -2673,7 +2718,11 @@ void
// by the 1995 compiler) (:15364-15368).
if (damage->surfaceNormal * worldLinearVelocity < -1.0e-4f)
return;
// DEFERRED: crunch TakeDamageMessage to the icon (:15369-15401).
// Crunch (:15369-15401): dispatch the collision damage to the icon
// (zone==-1) BEFORE the walk-through sentinel overwrites the amount.
// Buildings with damage zones resolve via their handler; crushable props
// have none -> the base handler no-ops. UNBLOCKED by STEP 6.
BTDispatchCollisionDamage(this, owner, damage, collision);
if (!stopping)
damage->damageAmount = 0.00123f; // walk-through sentinel 0x3aa137f4 (:15402-15404)
}