# Direct gyro crunch kicks: mech master performance FUN_004a9b5c (@4aa254/@4aa288 = alternate-gait engage jolt; @4aa342 = engaged-gait 0.4s rumble; @4aa81e/@4aa86c = crushable-icon crunch in the ground/collide response) + ProjectileWeapon::FireWeapon @4bc104 (@4bc194 = firing recoil kick). Verified NO gyro calls in the blocking-hit branch (@4aa88a-4aab5f) and NO kick in MissileLauncher::FireWeapon @4bcc60.

## semantics
CONTAINER 1: FUN_004a9b5c = Mech master per-frame performance, cdecl(Mech *this[ebp+8] -> ebx, Scalar dt[ebp+0xc]).  edi = *(mech+0x128) = controls-mapper subsystem (port: controlSource / controlsMapper, subsystemArray[0]).  gyro = mech+0x528 (port: gyroSubsystem).

=== BLOCK 1 (@4aa158-4aa2be): alternate-gait toggle + ENGAGE JOLT (sites @4aa254 + @4aa288) ===
if (airborneCycleRate@0x5bc != -1.1f) {            // fcomp const @0x4ab174 = 0xbf8ccccd = -1.1f; == -1.1 -> skip whole toggle ("mech has no alternate cycle rate" sentinel)
  if (legCycleSpeed@0x348 > 0.0f                   // fcomp @0x4ab178 = 0.0f
      && controlsMapper->speedDemand@+0x128 < 0.0f) {   // [edi+0x128] fcomp 0.0, jae -> disengage
    if (airborneSelect@0x3f4 == 0) {               // RISING EDGE only
      airborneSelect = 1;
      RequestActionFlags(0x100);                   // FUN_004a4c54(this, 0x100)
      forwardCycleRate@0x344 = airborneCycleRate@0x5bc;
      if (gyro@0x528) {
        Scalar r = RandomUnit();                   // FUN_00408050, [0,1)
        if (RandomUnit() > 0.5f) r = -r;           // const @0x4ab170 = 0.5f (random sign)
        // dir = (0, 0, r) -- Z-axis, random magnitude+sign
        Gyroscope::ApplyDamageTorque (gyro, 0,0,r, 0.4f);   // call @4aa254 -> 4b2de4, mag 0x3ecccccd
        Gyroscope::ApplyDamageImpulse(gyro, 0,0,r, 0.4f);   // call @4aa288 -> 4b2d8c, SAME vec
      }
    }
  } else {                                          // @4aa292 falling edge
    if (airborneSelect@0x3f4) { airborneSelect = 0; RequestActionFlags(0x100);
                                forwardCycleRate@0x344 = groundCycleRate@0x5b8; }
  }
}
SEMANTIC FLAG [T3]: the engage gate is (cycling && speedDemand < 0); by the port's own convention (negative demand = reverse throttle, mech4.cpp:2009) 0x3f4 reads as a REVERSE-gait select, so mech.hpp's names airborneSelect@0x3f4 / airborneCycleRate@0x5bc are suspect misnomers.  Mechanics above are exact [T1] regardless of the name.

=== BLOCK 2 (@4aa2be-4aa365): ENGAGED-GAIT RUMBLE (site @4aa342) ===
if (airborneSelect@0x3f4 && legCycleSpeed@0x348 > walkStrideLength@0x534 && gyro@0x528) {
  if (*(Scalar*)&this->0x5c4 > 0.0f)  *(Scalar*)&this->0x5c4 -= dt;       // countdown, [ebp+0xc]
  else { Gyroscope::ApplyDamageImpulse(gyro, 0,1,0, 0.2f);                // call @4aa342, dir=(0,1,0)=up, mag 0x3e4ccccd
         *(Scalar*)&this->0x5c4 = 0.4f; }                                 // period reset 0x3ecccccd @4aa347
}
FLAG: mech.hpp declares 0x5c4 as `int clipLoadGuard` -- the binary uses it here as a FLOAT rumble-period timer (mis-typed; needs a Scalar member).

=== BLOCK 3 (@4aa76c-4aa885): CRUSHABLE-ICON CRUNCH (sites @4aa81e + @4aa86c) -- the mech4.cpp:3310 dormant comment ===
Runs in the ground/collide response policy, ONLY on the sentinel branch:
if (dmg.damageAmount == 0.00123f) {                 // fcomp const @0x4ab180 = 0x3aa137f4 (crushable-CulturalIcon walk-through sentinel)
  worldLinearVelocity = savedWorldVel;  localOrigin.linearPosition = savedPos;   // restore POST-SNAP saves
  MoveCollisionVolume();                            // vtbl+0x34
  dmg.damageAmount = 0.0f;
  if (gyro@0x528) {
    Vector3D n = Normalize(dmg.damageForce);        // FUN_004087f4 on Damage+8; damageForce = DELTA worldLinearVelocity across the bounce, filled by engine Mover::ProcessCollisionList (MOVER.cpp:1299 `damage->damageForce.Subtract(worldLinearVelocity, initial_velocity)`) -- i.e. the bounce-impulse direction ("the 0.4*normal")
    Gyroscope::ApplyDamageTorque (gyro, n.x,n.y,n.z, 0.4f);   // call @4aa81e, mag 0x3ecccccd
    Gyroscope::ApplyDamageImpulse(gyro, 0,1,0,      0.2f);    // call @4aa86c, up, mag 0x3e4ccccd
  }
  AlarmIndicator::SetLevel(mech+0x450 /*heatAlarm*/, 0);      // FUN_0041bbd8 @4aa87d -- existing tail, not gyro
  goto end;                                          // jmp 4aab5f
}
The BLOCKING branch (damageAmount > 0.0, @4aa88a-4aab5f) has NO gyro calls (verified: only 41bbd8/408440/420ea4/4a7fc4/4a4c54 crash-anim calls).  Caveat: FUN_004087f4 divides by |v| UNGUARDED -- if Delta-v ~= 0 the original NaN'd too; a Small_Enough guard is a flaggable deviation.

CONTAINER 2: ProjectileWeapon::FireWeapon @4bc104 (vtable slot 18 / +0x48 in vtable 0x51242c; `this` = weapon [ebp+8] -> ebx; owner mech = [ebx+0xd0]).
=== BLOCK 4 (@4bc136-4bc19c): FIRING RECOIL KICK (site @4bc194) ===
if (HeatModelActive())                              // FUN_004ad7d4(this); gates ONLY the heat add
  currentHeat@0x1c8 += heatPerShot@0x3d8;
Mech *m = owner@0xd0;
if (damageData.damageAmount@0x3ac > 3.0f            // const @0x4bc3f4 = 3.0f (Damage embedded @0x3A8: type@0x3A8, amount@0x3AC)
    && m->gyroSubsystem@0x528) {
  Gyroscope::ApplyDamageImpulse(gyro, 0.0f, 0.6f, -1.5f,          // dir consts 0x3f19999a / 0xbfc00000
                                damageData.damageAmount * 0.0625f); // mag = amount/16, const @0x4bc3f8
}
then tracerCounter@0x408++, tracer modulo tracerInterval@0x438, matrix/muzzle spawn path...  The kick precedes ammo consumption in the binary (no early-out before it).  MissileLauncher::FireWeapon @4bcc60 does NOT chain to @4bc104 and has NO gyro kick (grep: no 0x528/FUN_004b2d8c refs; it uses +0x3ac only as per-missile damage).

## call_args
- placeholder

## port_integration
All target methods exist byte-exact in game/reconstructed/gyro.cpp (ApplyDamageImpulse @691, ApplyDamageTorque @706; signatures (Scalar x, Scalar y, Scalar z, Scalar magnitude) -- match the binary 5-dword cdecl frames).  Access pattern: mech.hpp:422 `Subsystem *gyroSubsystem; // @0x528`; either include gyro.hpp and static_cast<Gyroscope*>, or follow the existing complete-type bridge pattern (extern GyroFrameJointWrite, declared mech4.cpp:3166, defined gyro.cpp:487) with e.g. GyroApplyDamageImpulse/Torque(Subsystem*, Scalar,Scalar,Scalar,Scalar) bridges in gyro.cpp -- the bridge avoids the databinding trap and matches house style.

(1) CRUSHABLE-ICON CRUNCH (@4aa81e/@4aa86c) -> game/reconstructed/mech4.cpp:3304-3315, inside Mech::AuthenticGroundAndCollide's `if (dmg.damageAmount == 0.00123f)` branch, REPLACING the dormant comment at mech4.cpp:3310.  Exact insertion (after `dmg.damageAmount = 0.0f;`, before the GroundLog):
    if (gyroSubsystem) {
        Vector3D n; n.Normalize(dmg.damageForce);          // FUN_004087f4; damageForce already filled by engine Mover::ProcessCollisionList (engine/MUNGA/MOVER.cpp:1299)
        GyroApplyDamageTorque (gyroSubsystem, n.x, n.y, n.z, 0.4f);   // @4aa81e
        GyroApplyDamageImpulse(gyroSubsystem, 0.0f, 1.0f, 0.0f, 0.2f); // @4aa86c
    }
  dmg.damageForce needs no new plumbing -- ProcessCollisionList at mech4.cpp:3302 is the engine method that writes it.  Do NOT add a kick to the blocking-hit branch (mech4.cpp:3316+) -- binary-verified none exists.

(2) ALTERNATE-GAIT ENGAGE JOLT + RUMBLE (@4aa254/@4aa288/@4aa342) -> mech4.cpp local-drive path, immediately BEFORE the channel advance at mech4.cpp:2056-2058 (`adv = AdvanceBodyAnimation(dt, 1); legAdv = AdvanceLegAnimation(dt);`) -- the binary runs these two blocks (@4aa158-4aa365) directly before its IsDisabled + AdvanceLegAnimation sequence (@4aa365-4aa399).  All gate members exist in mech.hpp (airborneSelect, forwardCycleRate@0x344, legCycleSpeed@0x348, walkStrideLength@0x534, groundCycleRate@0x5b8, airborneCycleRate@0x5bc, RequestActionFlags @004a4c54); speedDemand is already read as controlsMapper->speedDemand at mech4.cpp:2008.  Two fixes needed: (a) the port currently has NO writer of airborneSelect -- this block IS the authentic writer (engage: legCycleSpeed>0 && speedDemand<0 && airborneCycleRate != -1.1f, +RequestActionFlags(0x100) +forwardCycleRate switch on BOTH edges); (b) mech+0x5c4 is declared `int clipLoadGuard` in mech.hpp:662 -- the binary uses it as a Scalar rumble timer here (add/re-type a Scalar member, e.g. gyroRumbleTimer).  RandomUnit exists (dmgtable.cpp:86 RandomUnit / projweap.cpp:486 UniformRandom = FUN_00408050).

(3) FIRING RECOIL KICK (@4bc194) -> game/reconstructed/projweap.cpp:603 ProjectileWeapon::FireWeapon, at the TOP (binary order: heat add, then kick, then tracer/spawn -- i.e. insert after Check(this), before ConsumeRound()):
    if (damageData.damageAmount > 3.0f && owner) {          // damageData @0x3A8 (mechweap.hpp:326), amount @0x3AC
        Subsystem *g = ((Mech *)owner)->gyroSubsystem;      // owner @0xd0 -> mech+0x528
        if (g) GyroApplyDamageImpulse(g, 0.0f, 0.6f, -1.5f, damageData.damageAmount * 0.0625f);
    }
  (Placing it before ConsumeRound matches the binary exactly; the binary has no early-out before the kick.)  Do NOT add the kick to MissileLauncher::FireWeapon (mislanch.cpp @4bcc60) -- binary-verified it has none.  The binary's heat add ([+0x3d8] -> [+0x1c8] gated by FUN_004ad7d4/HeatModelActive) precedes the kick; if the port's FireWeapon lacks it, that is a separate (pre-existing) gap, not part of this hook.

confidence: high
