From e40abc86ea5c392b6f97931cb585fc2d3c835e97 Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 3 Aug 2026 14:08:37 -0500 Subject: [PATCH] BT410 5.3.100: the collision economy -- armor is never touched by a crash, and the wall stops one-shotting the mech The 5.3.99 open question is closed the way the binary closes it, not with a divisor. Two pieces: THE DIVERT (damage hub @0x4a0368): type-0 Collision damage NEVER reaches the armor zones. Our handler was routing wall crashes through the same cylinder-lottery path as enemy fire, which is why 24k-point kinetic numbers one-shot vital zones. The binary short-circuits them into THE DISTRIBUTOR (@0049ffcc): the manual's "collision damage" technician setting. Gated on the owning player's advanced-damage flag; priced against a 100 km/h reference impact -- scale = (2000 / mass) / (100 km/h)^2 / (1 - elasticity^2) amount = raw * scale; under 0.5 the tap is FREE n = Round(amount * 2) sub-hits of amount/n each -- and applied as rattle crits to random INTERNAL subsystems (HeatSink / Gyroscope / Torso families only), drawn by cumulative collisionCriticalHitWeight against a [0,1) roll. An un-won roll lands nowhere; the binary does not normalize the weights, so neither do we. LIVE, same arena mission as 5.3.99's four-death run: [colldmg] rattle 0.677919 pts in 1 sub-hits (scale=3.6e-05) [crash] BLOCK dmg=18831.1 iv2=711.428 KNOCKDOWN [crash] BLOCK dmg=331.878 iv2=12.549 (x6, pressed against a wall) deaths: ZERO (was four) An 18.8k-point crash becomes 0.68 points of internal rattle; the low-speed wall grind prices as free taps under the 0.5 floor; the knockdown stagger still fires above the velocity threshold. Walls now block, stagger, and rattle -- they no longer execute. Plumbing: MechSubsystem::CollisionCritWeight() accessor (the weight was already streamed at +0x10C, protected), Mech::DistributeCollisionDamage declared, random.hpp include. Co-Authored-By: Claude Fable 5 --- restoration/source410/BT/MECH.CPP | 117 +++++++++++++++++++++++++++ restoration/source410/BT/MECH.HPP | 8 ++ restoration/source410/BT/MECHSUB.HPP | 8 ++ 3 files changed, 133 insertions(+) diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index 3583acf4..b6907c95 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -56,6 +56,9 @@ #if !defined(BOXSOLID_HPP) # include // BoxedSolid extents -- the ground-snap probe #endif +#if !defined(RANDOM_HPP) +# include // Random -- the collision-rattle draw +#endif #include // DamageLookupTable -- the cylinder hit table #include // Player::VehicleDeadMessage -- the death notification #include // BTPlayer::ScoreMessage -- the kill credit @@ -778,6 +781,106 @@ Mech::~Mech() // Entity handler which routes damageZones[zone]->TakeDamage. //############################################################################# // +// +//############################################################################# +// DistributeCollisionDamage (binary @0049ffcc, reached ONLY from the damage +// hub's type-0 divert) -- the collision-damage economy. +// +// Collision damage is the manual's "collision damage" TECHNICIAN SETTING: +// gated on the owning player's advanced-damage flag, priced against a +// 100 km/h reference impact, and applied as 0.5-point RATTLE CRITS to random +// internal subsystems. Armor is never touched by a collision. +// +// scale = (2000 / mass) / (100 km/h)^2 / (1 - elasticity^2) +// amount = raw * scale; under 0.5 the tap is FREE +// n = Round(amount * 2) sub-hits of amount/n each +// +// Each sub-hit lands on ONE roster subsystem drawn by cumulative +// collisionCriticalHitWeight against a [0,1) roll, restricted to the +// HeatSink / Gyroscope / Torso families. An un-won roll lands nowhere -- +// faithful: the binary does not normalize the weights. +//############################################################################# +// +void + Mech::DistributeCollisionDamage(Damage *damage) +{ + Check(this); + Check_Pointer(damage); + + BTPlayer + *player = (BTPlayer *)GetPlayerLink(); + if (player == NULL || !player->IsAdvancedDamageOn()) + { + return; + } + + Scalar + reference = 100.0f * 0.27777779f; // 100 km/h in world u/s + Scalar + denominator = + 1.0f - elasticityCoefficient * elasticityCoefficient; + Scalar + scale = ((2000.0f / moverMass) / (reference * reference)) / denominator; + + damage->damageAmount *= scale; + if (damage->damageAmount < 0.5f) + { + return; + } + + int + sub_hits = (int)(damage->damageAmount * 2.0f + 0.5f); + if (sub_hits < 1) + { + sub_hits = 1; + } + damage->damageAmount /= (Scalar)sub_hits; + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[colldmg] rattle " + << (damage->damageAmount * sub_hits) << " pts in " + << sub_hits << " sub-hits (scale=" << scale << ")" + << endl << flush; + } + + int + hit, + slot; + for (hit = 0; hit < sub_hits; ++hit) + { + Scalar + threshold = (Scalar)Random, + accumulated = 0.0f; + for (slot = 0; slot < GetSubsystemCount(); ++slot) + { + Subsystem + *subsystem = GetSubsystem(slot); + if (subsystem == NULL) + { + continue; + } + if ( + !subsystem->IsDerivedFrom(HeatSink::ClassDerivations) && + !subsystem->IsDerivedFrom(Gyroscope::ClassDerivations) && + !subsystem->IsDerivedFrom(Torso::ClassDerivations) + ) + { + continue; + } + accumulated += + ((MechSubsystem *)subsystem)->CollisionCritWeight(); + if (accumulated < threshold) + { + continue; + } + ((MechSubsystem *)subsystem)->ApplyDamageAndMeasure(*damage); + break; + } + } + Check_Fpu(); +} + void Mech::TakeDamageMessageHandler(TakeDamageMessage *message) { @@ -795,6 +898,20 @@ void lastInflictingID = message->inflictingEntity; lastInflictingDamage = message->damageData.damageAmount; + // + // THE COLLISION DIVERT (binary @0x4a0368 -> @0049ffcc): type-0 Collision + // damage NEVER reaches the armor zones. It is priced against a 100 km/h + // reference impact and applied as internal RATTLE crits -- which is the + // answer to the raw kinetic numbers being thousands of points where a + // PPC is ~12. Without this divert a hard wall crash one-shots a vital + // zone through the cylinder lottery (observed live, 5.3.99). + // + if (message->damageData.damageType == Damage::CollisionDamageType) + { + DistributeCollisionDamage(&message->damageData); + return; + } + // // The cylinder resolve (binary @0x4a0264 tail): an UNAIMED hit arrives // with invalidDamageZone set -- map its world impact point onto a hull diff --git a/restoration/source410/BT/MECH.HPP b/restoration/source410/BT/MECH.HPP index 326e40da..9e911224 100644 --- a/restoration/source410/BT/MECH.HPP +++ b/restoration/source410/BT/MECH.HPP @@ -378,6 +378,14 @@ unsigned callback_arg, Scalar carryover, int move_joints); + // + // The collision-damage economy (binary @0049ffcc): type-0 damage is + // diverted here by the damage hub and rattles internal subsystems -- + // armor is never touched by a collision. + // + void + DistributeCollisionDamage(Damage *damage); + static Scalar BodyClipFinished( Mech *mech, diff --git a/restoration/source410/BT/MECHSUB.HPP b/restoration/source410/BT/MECHSUB.HPP index 0bf69a32..d5a35991 100644 --- a/restoration/source410/BT/MECHSUB.HPP +++ b/restoration/source410/BT/MECHSUB.HPP @@ -175,6 +175,14 @@ Scalar ApplyDamageAndMeasure(Damage &damage); + // + // The collision-rattle draw weight (binary subsystem+0x10C) -- read + // by Mech::DistributeCollisionDamage's cumulative roll. + // + Scalar + CollisionCritWeight() const + { Check(this); return collisionCriticalHitWeight; } + Logical IsVitalSubsystem() const { Check(this); return vitalSubsystem; }