# Gyro damage fan-out FUN_004b2980 (0x4b2980-0x4b2d8b, unexported gap) — disassembled byte-exact via scratchpad/dis_4b2980.py from reference/decomp/section_dump.txt, plus its hub call site 0x4a02fb inside the take-damage hub FUN_004a0230

## semantics
SIGNATURE (cdecl, caller cleans 0x34 = 13 dwords; args are the engine Damage struct — engine/MUNGA/DAMAGE.h, T0 — flattened by value after `this`):

void GyroApplyDamageFanOut(Gyroscope *g /*ebp+8*/, Enumeration damageType /*+0xC*/, Scalar damageAmount /*+0x10*/, Vector3D damageForce /*+0x14..0x1C*/, Normal surfaceNormal /*+0x20..0x28 UNUSED*/, Point3D impactPoint /*+0x2C..0x34 UNUSED*/, int burstCount /*+0x38*/)

BODY (every step from the disassembly):
1. @4b298c: if (damageAmount == 0.0f) return;                      // const 0.0f @0x4b2d80
2. @4b299e: if (damageType == 0 /*Damage::CollisionDamageType*/) return;   // collision NEVER bounces via this path (jump-table case 0 that zeroes all four responses is dead code behind this early return)
3. @4b29a9: Scalar trans=0 /*ebp-0x10*/, pitchRoll=0 /*-0xC*/, yaw=0 /*-8*/, vibration=0 /*-4*/;
4. Vector3D dir; /* ebp-0x1C: x@-0x1C y@-0x18 z@-0x14 */
   @4b29bd: if (VecEqualWithinEps(&damageForce, &kZeroVec3 /*global 0x4e0f74 = (0,0,0)*/, 1e-4f /*imm 0x38d1b717*/))   // FUN_004084fc: |dx|<=eps && |dy|<=eps && |dz|<=eps -> 1
   {   // RANDOM horizontal direction; RandomUnit = FUN_00408050 = (float)laggedXorRand() * 1/32768 (0x38000000 @0x408078) -> [0,1); TWO rolls per component (sign roll, then value roll)
       @4b29d7: dir.x = (RandomUnit() >= 0.5f /*@0x4b2d84*/) ? RandomUnit() : -RandomUnit();   // fchs on the < branch
       @4b2a0d: dir.z = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit();
       @4b2a43: dir.y = 0.0f;
   } else
       @4b2a4a: dir = damageForce;                                  // FUN_00408440 vec3 copy
5. @4b2a62: Normalize(dir, dir);                                    // FUN_004087f4 (dst,src) = src/|src|
6. @4b2a67: g->placeRot.y /*0x2AC*/ = *(g->externalPitchPtr /*0x258*/);  // = torso currentTwist
   g->placeRot.x /*0x2A8*/ = 0;  g->placeRot.z /*0x2B0*/ = 0;
7. @4b2a90: QuatFromRotation(&g->placeQuat /*0x298*/, &g->placeRot);      // FUN_00409a00
8. @4b2aa8: g->placePos /*0x28C*/ = kZeroVec3;                            // vec3 copy from 0x4e0f74
9. @4b2ac0: Mat34FromPlacement(&g->workMatrix /*0x25C*/, &g->placePos);   // FUN_0040ab44; placement = {pos@+0, quat@+0xC} (0x298-0x28C=0xC checks)
10. @4b2ac7: Vector3D tmp /*ebp-0x28*/ = dir;  Vec3TimesMat34Rows(&dir, &tmp, &g->workMatrix);  // FUN_00408744 — rotate the world-space hit direction by yaw=torsoTwist into the torso/body frame
11. @4b2aed: Normalize(dir, dir);
12. @4b2afd: switch (damageType)  // jump table @0x4b2b10, cases 0..4; damageType>4 falls through with all four responses = 0
    case 1 /*Ballistic, @4b2b3d*/:   each computed independently as fld amount; fdiv mult; fmul resp:
        trans     = damageAmount / g->damageMultiplier[1] /*0x330*/ * g->damageResponse[1].trans     /*0x350*/;
        pitchRoll = damageAmount / g->damageMultiplier[1]          * g->damageResponse[1].pitchRoll /*0x354*/;
        yaw       = damageAmount / g->damageMultiplier[1]          * g->damageResponse[1].yaw       /*0x358*/;
        vibration = damageAmount / g->damageMultiplier[1]          * g->damageResponse[1].vibration /*0x35C*/;
    case 2 /*Explosive, @4b2b8a*/:  ONLY case that reads burstCount (fild ebp+0x38 FIRST, then fmul amount):
        trans     = (Scalar)burstCount * damageAmount / g->damageMultiplier[2] /*0x334*/ * g->damageResponse[2].trans /*0x360*/;   (…same pattern for .pitchRoll 0x364 / .yaw 0x368 / .vibration 0x36C)
    case 3 /*Laser, @4b2be3*/:    damageAmount / g->damageMultiplier[3] /*0x338*/ * g->damageResponse[3].{trans 0x370, pitchRoll 0x374, yaw 0x378, vibration 0x37C};
    case 4 /*Energy, @4b2c2d*/:   damageAmount / g->damageMultiplier[4] /*0x33C*/ * g->damageResponse[4].{trans 0x380, pitchRoll 0x384, yaw 0x388, vibration 0x38C};
13. @4b2c75: upper-clamp each of the four independently: if (v > 1.3f /*0x3fa66666 @0x4b2d88*/) v = 1.3f;   // no lower clamp
14. Four calls, each cdecl (this, x, y, z, magnitude), add esp 0x14:
    @4b2d00: g->ApplyDamageImpulse(dir.x, dir.y, dir.z, trans);        // translational eye kick along the body-frame hit direction
    @4b2d23: g->ApplyDamageTorque (dir.x, dir.y, dir.z, pitchRoll);
    @4b2d4b: g->ApplyDamageImpulse(vibDir.x, vibDir.y, vibDir.z, vibration);  // SECOND impulse call: direction = the vec3 at gyro+0x390 — WHY TWO: call 1 is the directional knock (resp.trans), call 2 is the vibration shake along the FIXED member vector at +0x390, ctor-initialized (0,1,0) = straight up.  HEADER FIX NEEDED: gyro.hpp:323-325 currently names +0x390/+0x394/+0x398 as scalars animationOffset(0)/animationScale(1)/animationPhase(0) — the binary reads them as one Vector3D; the 0/1/0 init IS the unit +Y vibrationDirection.  Rename to `Vector3D vibrationDirection; // @0x390 init (0,1,0)`.
    @4b2d6e: g->ApplyVerticalImpulse(dir.x, dir.y, dir.z, yaw);        // NOTE: the response row's third field (offset +8, named `yaw` in our header) is what feeds ApplyVerticalImpulse (pitch-only bodyForce)

CALL SITE @0x4a02fb (inside hub FUN_004a0230, args: Mech* this@ebp+8 in ebx, TakeDamageMessage* @ebp+0xC in esi — this hub IS the take-damage entry; the gyro kick is its FIRST action):
- @4a0242: if ((mech->flags@+0x28 & 0xC) == 4) Warn("Replicant Mech recieving takedamagemessage!", "d:\\tesla_bt\\bt\\MECH.CPP", 986 /*0x3DA*/) via FUN_0040385c — warning only, does NOT return;
- @4a0264: if (mech->gyroSubsystem /*mech+0x528*/ != 0)  FUN_004b2980(gyro, <12 dwords copied verbatim from &msg->damageData at msg+0x2C>);  add esp,0x34;
- then the rest of the hub (@4a0303: FUN_0049fb54(mech) bool check, …) — the gyro kick precedes ALL zone routing and is independent of damageZone validity.

## call_args
- [esp+0x00] Gyroscope* this -> the mech's gyro subsystem -> ecx = [ebx+0x528] (Mech::gyroSubsystem, mech.hpp:422), pushed last, gated non-null @4a0264
- [esp+0x04] Enumeration damageType -> Damage::damageType (0=Collision 1=Ballistic 2=Explosive 3=Laser 4=Energy, DAMAGE.h) -> [eax] where eax = esi+0x2C = &message->damageData (message in esi)
- [esp+0x08] Scalar damageAmount -> Damage::damageAmount -> [eax+4] = message+0x30
- [esp+0x0C..0x14] Vector3D damageForce (by value, x/y/z) -> Damage::damageForce = the hit direction; if ~zero the callee rolls a random horizontal direction -> [eax+8/0xC/0x10] = message+0x34/0x38/0x3C via ebp-4 pointer temp
- [esp+0x18..0x20] Normal surfaceNormal (by value) -> Damage::surfaceNormal -> [eax+0x14/0x18/0x1C] = message+0x40/0x44/0x48 via ebp-8 temp -- passed but NEVER READ by FUN_004b2980
- [esp+0x24..0x2C] Point3D impactPoint (by value) -> Damage::impactPoint -> [eax+0x20/0x24/0x28] = message+0x4C/0x50/0x54 via ebp-0xC temp -- passed but NEVER READ
- [esp+0x30] int burstCount -> Damage::burstCount (missile volley count; multiplies damage ONLY in the Explosive case) -> [eax+0x2C] = message+0x58
- cleanup: add esp,0x34 = 13 dwords, confirming cdecl with the whole Damage struct flattened by value after this

## port_integration
WHERE: game/reconstructed/mech.cpp:583 `Mech::TakeDamageMessageHandler(TakeDamageMessage *message)` — the binary's hub FUN_004a0230 kicks the gyro FIRST, before any zone resolution, so add the call near the top of our handler (after the Check/diag block, before the cylinder zone-resolve at mech.cpp:618 and before `Entity::TakeDamageMessageHandler(message)` at mech.cpp:630).  The gate is exactly `gyroSubsystem != 0` (already a named member, bound at mech.cpp:1036; external pitch already bound at mech.cpp:1155 via GyroBindExternalPitch so step 6's *externalPitchPtr read is live).  EXACT CALL TO ADD (bridge-fn pattern, same as GyroBindExternalPitch/GyroFrameJointWrite, because mech.cpp must not touch Gyroscope's layout — databinding trap):  in mech.cpp:  `if (gyroSubsystem != 0) GyroApplyDamage(gyroSubsystem, message->damageData);`  and in gyro.cpp (complete-type TU) add the fan-out as a Gyroscope method `void Gyroscope::ApplyDamage(Enumeration damageType, Scalar damageAmount, Vector3D damageForce, int burstCount)` implementing the exact_semantics above (surfaceNormal/impactPoint are unread — safe to drop from the port signature), plus the bridge `void GyroApplyDamage(Subsystem *s, const Damage &d) { static_cast<Gyroscope*>(s)->ApplyDamage(d.damageType, d.damageAmount, d.damageForce, d.burstCount); }`.  RandomUnit()/[0,1) already exists (mechrecon.hpp:98, RANDOM.h contract == FUN_00408050).  REQUIRED HEADER FIX: gyro.hpp:323-325 — replace the three scalars animationOffset/animationScale/animationPhase @0x390/394/398 with `Vector3D vibrationDirection;  // @0x390 ctor init (0,1,0)` (gyro.cpp:309-311 init collapses to one vec3 assignment); the fan-out's second ApplyDamageImpulse reads it as a vec3.  IMPORTANT SCOPE NOTE: CollisionDamageType (0) early-returns in FUN_004b2980, so the collision/ground crunch bounce does NOT come from this fan-out — the dormant 'gyro crunch feed (0.4*normal + 0.2*up)' at mech4.cpp:3310 corresponds to the DIRECT Apply* calls in the 0x4aa254/0x4aa288/0x4aa342/0x4aa81e/0x4aa86c region and 0x4bc194 (separate reconstruction; do not route collisions through ApplyDamage).  Also note: on a replicant ((flags&0xC)==4) the binary only WARNS ('Replicant Mech recieving takedamagemessage!' MECH.CPP:986) and still proceeds — no replicant gate needed around the gyro kick.

confidence: high
