BT410 5.3.102: ProcessCollision -- trees fall, walls do not, and both sides of a ram hurt

The per-contact responder (@004abb40), overriding the engine's protected
virtual per the MECH4.NOTES.md decode:

  THE SEPARATING GATE: a contact whose relative velocity points away from
  the surface ((normal . rel) < -1e-4) is skipped entirely -- no damage
  while pulling apart.

  MECH-VS-MECH: another mech takes the collision damage too, zone -1, its
  own cylinder resolving where the ram landed.  Both sides of a ram hurt,
  and both route through the 5.3.100 economy, so a ram rattles rather than
  executes.

  THE CRUSHABLE SENTINEL: a CulturalIcon takes its crunch dispatch, and one
  WITHOUT the StoppingCollisionVolume flag then sets the amount to exactly
  0.00123f -- Simulate's response block reads that value as "the move
  stands", restores the post-snap saves, and drives on through.  Trees
  fall; walls do not.

  THE CONTACT ACCUMULATOR feeds the audio-bound collisionState indicator
  (0->1 fires the impact sound once per contact, not per frame) and
  captures the impact speed for the authored volume/brightness scales.
  [T3 simplification, tagged in source: the authentic InitialHit/Slide
  split keys StaticBounce's OUT coefficients against an unidentified
  literal @0x4ac048.]

LIVE, one arena run, all three behaviours in the wild and zero deaths:

    [crash] CRUNCH (crushable) at (371.551,-102.862)     <- drove through it
    [colldmg] rattle 1.72691 pts in 3 sub-hits
    [crash] BLOCK dmg=47969.6 iv2=932.659 KNOCKDOWN      <- wall still walls
    [crash] BLOCK dmg=630 iv2=12.5 (x5, free taps)       <- grind still free

STAGED remainder, named in MECH4.NOTES.md: the gyro crunch feed (torque 0.4
/ impulse 0.2 -- the gyro is a feel-wave stub), and the authentic
InitialHit/Slide split pending the @0x4ac048 literal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-03 14:24:32 -05:00
co-authored by Claude Fable 5
parent 3790262aad
commit 05392e227b
2 changed files with 218 additions and 1 deletions
+200 -1
View File
@@ -59,6 +59,9 @@
#if !defined(RANDOM_HPP)
# include <random.hpp> // Random -- the collision-rattle draw
#endif
#if !defined(CULTURAL_HPP)
# include <cultural.hpp> // CulturalIcon -- the crunch / crushable sentinel
#endif
#include <dmgtable.hpp> // DamageLookupTable -- the cylinder hit table
#include <player.hpp> // Player::VehicleDeadMessage -- the death notification
#include <btplayer.hpp> // BTPlayer::ScoreMessage -- the kill credit
@@ -256,6 +259,7 @@ Mech::Mech(
legResetLatch = 0;
bodyResetLatch = 0;
limpModeOverride = 0;
collisionTemporaryState = 0;
{
const char *fl = getenv("BT_FORCE_LIMP");
if (fl != NULL && (*fl == '3' || *fl == '4'))
@@ -781,6 +785,166 @@ Mech::~Mech()
// Entity handler which routes damageZones[zone]->TakeDamage.
//#############################################################################
//
//
//#############################################################################
// ProcessCollision (binary @004abb40) -- the per-contact collision
// responder, overriding the engine's protected virtual. The base head is
// Mover::ProcessCollision verbatim (MOVER.CPP:1354); what the Mech adds is
// WHO it hit:
//
// * A separating contact -- one whose relative velocity points away from
// the surface -- is skipped entirely (the -1e-4 gate). No damage while
// pulling apart.
//
// * Another MECH takes the collision damage too (zone -1: the victim's
// own cylinder resolves it). Both sides of a ram hurt.
//
// * A CulturalIcon takes the crunch, and a CRUSHABLE one (no
// StoppingCollisionVolume flag) then sets the amount to the 0.00123f
// WALK-THROUGH SENTINEL -- Simulate's response block reads exactly that
// value as "the move stands": the mech drives THROUGH the prop while
// the prop takes the damage. Trees fall, walls do not.
//#############################################################################
//
void
Mech::ProcessCollision(
Scalar time_slice,
BoxedSolidCollision &collision,
const Point3D &old_position,
Damage *damage
)
{
Check(this);
Verify(time_slice > 0.0f);
Check(&collision);
Check_Pointer(damage);
Scalar
penetration;
if (
!collisionVolume->ProcessCollision(
collision,
worldLinearVelocity,
lastCollisionList,
&damage->surfaceNormal,
&penetration)
)
{
return;
}
Max_Clamp(penetration, time_slice);
Scalar
ratio = penetration / time_slice,
elasticity = elasticityCoefficient,
friction = frictionCoefficient;
Simulation
*owner_simulation = collision.GetTreeVolume()->GetOwningSimulation();
damage->damageAmount =
StaticBounce(
old_position,
time_slice,
ratio,
damage->surfaceNormal,
&elasticity,
minimumBounceSpeed,
&friction);
Entity
*other = (Entity *)owner_simulation;
if (other != NULL && other->IsDerivedFrom(Mover::ClassDerivations))
{
Vector3D
relative;
relative.Subtract(
worldLinearVelocity,
((Mover *)other)->GetWorldLinearVelocity());
if (damage->surfaceNormal * relative < -1.0e-4f)
{
return;
}
if (other->IsDerivedFrom(Mech::ClassDerivations))
{
Damage
ram = *damage;
ram.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);
Entity::TakeDamageMessage
hit(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
GetEntityID(),
-1,
ram
);
other->Dispatch(&hit);
}
}
if (other != NULL && other->IsDerivedFrom(CulturalIcon::ClassDerivations))
{
Logical
stopping =
((CulturalIcon *)other)->IsStoppingCollisionVolume();
//
// The separating gate against a STATIC icon (its velocity is zero,
// which the 1995 compiler inlined away).
//
if (damage->surfaceNormal * worldLinearVelocity < -1.0e-4f)
{
return;
}
//
// The crunch goes out BEFORE the sentinel overwrites the amount.
//
{
Damage
crunch = *damage;
crunch.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);
Entity::TakeDamageMessage
hit(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
GetEntityID(),
-1,
crunch
);
other->Dispatch(&hit);
}
if (!stopping)
{
damage->damageAmount = 0.00123f;
}
}
//
// The contact-state accumulator. [T3 simplification, per the decomp's
// dominant branch: the authentic InitialHit(1)/Slide(2) split keys the
// StaticBounce OUT coefficients against an unidentified literal
// (@0x4ac048); a resolved contact IS an impact, so accumulate 1 and
// capture the impact speed for the audio scales.]
//
if (collisionTemporaryState == 0)
{
collisionTemporaryState = 1;
collisionSpeed = worldLinearVelocity.Length();
}
}
//
//#############################################################################
// DistributeCollisionDamage (binary @0049ffcc, reached ONLY from the damage
@@ -1741,11 +1905,17 @@ void
{
Vector3D
impact_velocity = worldLinearVelocity;
Vector3D
post_snap_velocity = worldLinearVelocity;
Point3D
post_snap_position = localOrigin.linearPosition;
BoxedSolidCollisionList
*collisions = GetCurrentCollisions();
Damage
collision_damage;
collisionTemporaryState = 0;
if (collisions != NULL)
{
ProcessCollisionList(
@@ -1753,7 +1923,36 @@ void
&collision_damage);
}
if (collision_damage.damageAmount > 0.0f)
//
// 0->1 on a fresh contact fires the impact sound; SetState only
// fires on change, so a pressed-against contact plays once.
//
collisionState.SetState((unsigned)collisionTemporaryState);
if (collision_damage.damageAmount == 0.00123f)
{
//
// The crushable-icon WALK-THROUGH sentinel (ProcessCollision):
// the move STANDS -- restore the post-snap saves and undo the
// bounce. The prop took its crunch in the dispatch; the mech
// drives on through. (The gyro crunch feed -- torque 0.4 along
// the normalized force, upward impulse 0.2, binary
// @4aa81e/@4aa86c -- is STAGED with the gyro feel wave.)
//
worldLinearVelocity = post_snap_velocity;
localOrigin.linearPosition = post_snap_position;
localToWorld = localOrigin;
MoveCollisionVolume();
collision_damage.damageAmount = 0.0f;
if (getenv("BT_MECH_LOG"))
{
DEBUG_STREAM << "[crash] CRUNCH (crushable) at ("
<< post_snap_position.x << ","
<< post_snap_position.z << ")" << endl << flush;
}
}
else if (collision_damage.damageAmount > 0.0f)
{
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
localOrigin.linearPosition = frame_start_position;
+18
View File
@@ -386,6 +386,24 @@
void
DistributeCollisionDamage(Damage *damage);
//
// The per-contact collision responder (binary @004abb40) -- the
// override of the engine's protected virtual. Base resolution plus:
// the separating-contact gate, the mech-vs-mech ram dispatch, the
// CulturalIcon crunch, and the 0.00123f walk-through sentinel for
// crushable props. See MECH4.NOTES.md for the decode.
//
void
ProcessCollision(
Scalar time_slice,
BoxedSolidCollision &collision,
const Point3D &old_position,
Damage *damage);
int collisionTemporaryState; // binary mech+0x44c -- the frame's
// contact accumulator; pushed into
// collisionState once per frame
static Scalar
BodyClipFinished(
Mech *mech,