The recovered system: fire channels = LBE4ControlsManager buttonGroups (0x40/0x45/0x46/0x47); default groups = the per-mech type-6 controls-map resource in BTL4.RES, installed by the T0 CreateStreamedMappings the port already called -- it needed only the TriggerState attribute (id 0x13 PINNED to the binary value; fireImpulse@0x31C is the binary's TriggerState) and an input feed. Keyboard/harness now push press/release edges into the button groups; the gBT*Trigger bypasses, per-type keyboard split and 1,0 pulse hack are retired -- weapons sharing a button fire TOGETHER (madcat Trigger = 4 weapons). Myomers @4b9550/@4b95b8 misattribution corrected (they are MechWeapon ConfigureMappables/ChooseButton). Verified 2-node: kill through the authentic chain (12 hits vs ~36 pre-groups). Config-mode session (regrouping UI) = the remaining stage, KB-scoped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
319 lines
20 KiB
Markdown
319 lines
20 KiB
Markdown
All investigator claims re-verified byte-exact against `section_dump.txt` (my own capstone scripts: `C:\git\bt411\scratchpad\dis_4b2980.py`, `C:\git\bt411\scratchpad\dis_range.py`). Constants confirmed from the dump: `0x4b2d80=0.0f, 0x4b2d84=0.5f, 0x4b2d88=1.3f (0x3FA66666)`, `0x4ab170=0.5f, 0x4ab174=-1.1f, 0x4ab178=0.0f`, `0x4bc3f4=3.0f, 0x4bc3f8=0.0625f`. No disagreements between investigators survived re-reading; one nuance sharpened below (the two random-sign idioms differ: fan-out `jb` = "first roll < 0.5 → negative value roll"; gait jolt = "value roll first, negate if SECOND roll > 0.5 strict").
|
||
|
||
# IMPLEMENTATION PLAN — damage→gyro fan-out (FUN_004b2980) + the direct crunch kicks
|
||
|
||
## 0. Prerequisite header fixes (do these first)
|
||
|
||
**(F1) `C:\git\bt411\game\reconstructed\gyro.hpp:323-325`** — the binary reads `gyro+0x390..0x398` as ONE `Vector3D` (disasm @4b2d2b: `lea eax,[ebx+0x390]` then loads `[eax]/[eax+4]/[eax+8]` as the 3rd call's direction). Replace the three scalars:
|
||
```cpp
|
||
// @0x390 (this[0xE4..0xE6]) -- the VIBRATION AXIS: ctor init (0,1,0) =
|
||
// straight up. Read as one Vector3D by ApplyDamageResponse @4b2d2b
|
||
// (the second ApplyDamageImpulse = the vibration shake). [T1]
|
||
// (Was mis-split as scalars animationOffset/animationScale/animationPhase.)
|
||
Vector3D vibrationDirection; // @0x390 init (0.0f, 1.0f, 0.0f)
|
||
```
|
||
and `gyro.cpp:309-311` →
|
||
```cpp
|
||
vibrationDirection = Vector3D(0.0f, 1.0f, 0.0f); // @0x390 (0,1,0 = up)
|
||
```
|
||
Add to the lock block (gyro.cpp:~180): `static_assert(offsetof(Gyroscope, vibrationDirection) == 0x390, "vibrationDirection @0x390");` (no other code reads the old names — grep-verified).
|
||
|
||
**(F2) `C:\git\bt411\game\reconstructed\mech.hpp:662`** — `int clipLoadGuard; // @0x5c4` is used by the binary as a FLOAT rumble-period timer (`fld [ebx+0x5c4]; fcomp 0.0` @4aa2eb, `fsubr` @4aa359, stores `0x3ecccccd` @4aa347). Re-type/rename:
|
||
```cpp
|
||
Scalar gyroRumbleTimer; // @0x5c4 binary: float rumble-period countdown
|
||
// (engaged-gait rumble @4aa2eb-4aa35f);
|
||
// reset to 0 at clip load (mech3.cpp:330)
|
||
```
|
||
Update the only other writer, `mech3.cpp:330`: `gyroRumbleTimer = 0.0f;` (bit-identical to the old int 0 write).
|
||
|
||
## 1. `Gyroscope::ApplyDamageResponse` — the byte-exact FUN_004b2980 body
|
||
|
||
**Decl** (`gyro.hpp`, after `ApplyVerticalImpulse` at :214; add `class Damage;` fwd-decl at the top if not in scope):
|
||
```cpp
|
||
// @004b2980 -- the damage->gyro fan-out. The binary passes the whole
|
||
// Damage BY VALUE (12 dwords, caller add esp,0x34) but never mutates it
|
||
// and never reads surfaceNormal/impactPoint -- const-ref is byte-equal.
|
||
void ApplyDamageResponse(const Damage &damage);
|
||
```
|
||
|
||
**Body** (`gyro.cpp`, place directly after `ApplyVerticalImpulse` at :729). Engine ops are the authentic implementations: `Close_Enough(Vector3D,Vector3D,e)` == FUN_004084fc, `Vector3D::Normalize` == FUN_004087f4, `Quaternion::operator=(EulerAngles)` == FUN_00409a00, `AffineMatrix::operator=(Origin)` == FUN_0040ab44 (placement `{pos@+0, quat@+0xC}` IS the engine `Origin` layout, ORIGIN.h:15-16), the mechrecon `FUN_00408744` row-dot (verified in IntegrateMotion) == the dir rotate. `AffineMatrix` is exactly `Scalar entries[12]` (AFFNMTRX.h:21) and `ReconMatrix` wraps only it, so the casts below are layout-exact (lock them):
|
||
|
||
```cpp
|
||
// TU-local layout locks for the casts in ApplyDamageResponse:
|
||
static_assert(sizeof(AffineMatrix) == 12 * sizeof(Scalar), "AffineMatrix == 12 Scalars");
|
||
static_assert(sizeof(ReconMatrix) == sizeof(AffineMatrix), "ReconMatrix is a bare AffineMatrix");
|
||
|
||
//
|
||
// @004b2980 [CONFIDENT -- re-disassembled byte-exact] -- the damage->gyro
|
||
// fan-out: normalize the hit direction (RANDOM horizontal if ~zero), rotate it
|
||
// by the yaw-only torso-twist frame, scale by the per-damage-type
|
||
// multiplier/response curves, clamp at 1.3, fire the four Apply* kicks.
|
||
//
|
||
void
|
||
Gyroscope::ApplyDamageResponse(const Damage &damage)
|
||
{
|
||
// @4b298c: zero damage no-ops (const 0.0f @0x4b2d80)
|
||
if (damage.damageAmount == 0.0f)
|
||
return;
|
||
// @4b299e: Collision(0) NEVER bounces via this path (the jump-table case 0
|
||
// that zeroes the responses @4b2b24 is dead code behind this early return)
|
||
if (damage.damageType == Damage::CollisionDamageType)
|
||
return;
|
||
|
||
Scalar trans = 0.0f; // ebp-0x10
|
||
Scalar pitchRoll = 0.0f; // ebp-0x0C
|
||
Scalar yaw = 0.0f; // ebp-0x08
|
||
Scalar vibration = 0.0f; // ebp-0x04
|
||
|
||
// @4b29bd: FUN_004084fc(damageForce, ZeroVector@0x4e0f74, 1e-4f@0x38d1b717).
|
||
// A ~zero force rolls a RANDOM horizontal direction: per component, a SIGN
|
||
// roll first (jb @4b29eb: first roll < 0.5f@0x4b2d84 -> the value roll is
|
||
// negated), y = 0. RNG = FUN_00408050 (stream 0x521f5c) == RandomUnit().
|
||
Vector3D dir; // ebp-0x1C
|
||
if (Close_Enough(damage.damageForce, Vector3D(0.0f, 0.0f, 0.0f), 1e-4f))
|
||
{
|
||
dir.x = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit(); // @4b29d7
|
||
dir.z = (RandomUnit() >= 0.5f) ? RandomUnit() : -RandomUnit(); // @4b2a0d
|
||
dir.y = 0.0f; // @4b2a43
|
||
}
|
||
else
|
||
{
|
||
dir = damage.damageForce; // @4b2a4a FUN_00408440
|
||
}
|
||
dir.Normalize(dir); // @4b2a62 FUN_004087f4 (unguarded in the binary too)
|
||
|
||
// @4b2a67-4b2ac2: yaw-only body frame from the torso twist.
|
||
// placeRot = (0, *externalPitchPtr, 0) gyro+0x2A8/0x2AC/0x2B0
|
||
// placeQuat = QuatFromRotation(placeRot) FUN_00409a00
|
||
// placePos = Zero @4b2aa8
|
||
// workMatrix = MatrixFromPlacement FUN_0040ab44 (== Origin assign)
|
||
placeRot.y = *externalPitchPtr; // *(gyro+0x258) = torso currentTwist
|
||
placeRot.x = 0.0f;
|
||
placeRot.z = 0.0f;
|
||
Origin frame;
|
||
frame.angularPosition = EulerAngles(
|
||
Radian(placeRot.x), Radian(placeRot.y), Radian(placeRot.z)); // FUN_00409a00
|
||
placeQuat[0] = frame.angularPosition.x; // member-state fidelity @0x298
|
||
placeQuat[1] = frame.angularPosition.y;
|
||
placeQuat[2] = frame.angularPosition.z;
|
||
placeQuat[3] = frame.angularPosition.w;
|
||
placePos = Vector3D(0.0f, 0.0f, 0.0f);
|
||
frame.linearPosition = Point3D(placePos.x, placePos.y, placePos.z);
|
||
*(AffineMatrix *)workMatrix = frame; // @4b2ac2 FUN_0040ab44
|
||
|
||
// @4b2ac7-4b2af5: rotate the WORLD hit direction into the torso/body frame
|
||
// (FUN_00408744 row-dot: out[i] = sum_j v[j]*M(i,j)) + re-normalize.
|
||
Vector3D tmp = dir; // ebp-0x28
|
||
FUN_00408744(&dir, (const Scalar *)&tmp, (ReconMatrix *)workMatrix);
|
||
dir.Normalize(dir); // @4b2aed
|
||
|
||
// @4b2afd: per-type scaling (jump table @0x4b2b10; type > 4 leaves all four
|
||
// zero). Each term computed INDEPENDENTLY as amount / multiplier * response
|
||
// (same expression shape as the binary's fld/fdiv/fmul per term).
|
||
switch (damage.damageType)
|
||
{
|
||
case Damage::BallisticDamageType: // @4b2b3d mult@0x330 resp@0x350
|
||
trans = damage.damageAmount / damageMultiplier[1] * damageResponse[1].trans;
|
||
pitchRoll = damage.damageAmount / damageMultiplier[1] * damageResponse[1].pitchRoll;
|
||
yaw = damage.damageAmount / damageMultiplier[1] * damageResponse[1].yaw;
|
||
vibration = damage.damageAmount / damageMultiplier[1] * damageResponse[1].vibration;
|
||
break;
|
||
case Damage::ExplosiveDamageType: // @4b2b8a the ONLY case reading burstCount (fild first)
|
||
trans = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].trans;
|
||
pitchRoll = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].pitchRoll;
|
||
yaw = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].yaw;
|
||
vibration = (Scalar)damage.burstCount * damage.damageAmount / damageMultiplier[2] * damageResponse[2].vibration;
|
||
break;
|
||
case Damage::LaserDamageType: // @4b2be3 mult@0x338 resp@0x370
|
||
trans = damage.damageAmount / damageMultiplier[3] * damageResponse[3].trans;
|
||
pitchRoll = damage.damageAmount / damageMultiplier[3] * damageResponse[3].pitchRoll;
|
||
yaw = damage.damageAmount / damageMultiplier[3] * damageResponse[3].yaw;
|
||
vibration = damage.damageAmount / damageMultiplier[3] * damageResponse[3].vibration;
|
||
break;
|
||
case Damage::EnergyDamageType: // @4b2c2d mult@0x33C resp@0x380
|
||
trans = damage.damageAmount / damageMultiplier[4] * damageResponse[4].trans;
|
||
pitchRoll = damage.damageAmount / damageMultiplier[4] * damageResponse[4].pitchRoll;
|
||
yaw = damage.damageAmount / damageMultiplier[4] * damageResponse[4].yaw;
|
||
vibration = damage.damageAmount / damageMultiplier[4] * damageResponse[4].vibration;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// @4b2c75-4b2ce2: upper-clamp each at 1.3f (0x3FA66666 @0x4b2d88); NO lower clamp.
|
||
if (trans > 1.3f) trans = 1.3f;
|
||
if (pitchRoll > 1.3f) pitchRoll = 1.3f;
|
||
if (yaw > 1.3f) yaw = 1.3f;
|
||
if (vibration > 1.3f) vibration = 1.3f;
|
||
|
||
// The four kicks. Call 3 is the vibration SHAKE along the fixed member
|
||
// axis vibrationDirection@0x390 (up); call 4 feeds the response row's
|
||
// third field ("yaw") into the pitch-only vertical impulse.
|
||
ApplyDamageImpulse (dir.x, dir.y, dir.z, trans); // @4b2d00
|
||
ApplyDamageTorque (dir.x, dir.y, dir.z, pitchRoll); // @4b2d23
|
||
ApplyDamageImpulse (vibrationDirection.x, vibrationDirection.y,
|
||
vibrationDirection.z, vibration); // @4b2d4b
|
||
ApplyVerticalImpulse(dir.x, dir.y, dir.z, yaw); // @4b2d6e
|
||
}
|
||
```
|
||
(If `Origin` isn't pulled in via `bt.hpp`, add `#include <ORIGIN.hpp>` to gyro.cpp's include block. `RandomUnit` comes via mech.hpp→mechrecon.hpp:104, backed by the engine `RandomGenerator` == FUN_00408050's contract.)
|
||
|
||
**Bridges** (gyro.cpp, after `GyroBindExternalPitch` at :908 — the databinding-trap pattern; Gyroscope is complete only in this TU):
|
||
```cpp
|
||
//===========================================================================//
|
||
// Damage->gyro bridges (binary call sites: hub @4a02fb; performance crunches
|
||
// @4aa254/@4aa288/@4aa342/@4aa81e/@4aa86c; weapon recoil @4bc194).
|
||
//===========================================================================//
|
||
void GyroApplyDamage(Subsystem *gyro, const Damage &damage)
|
||
{
|
||
if (gyro != 0)
|
||
static_cast<Gyroscope *>(gyro)->ApplyDamageResponse(damage);
|
||
}
|
||
void GyroApplyDamageImpulse(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar magnitude)
|
||
{
|
||
if (gyro != 0)
|
||
static_cast<Gyroscope *>(gyro)->ApplyDamageImpulse(x, y, z, magnitude);
|
||
}
|
||
void GyroApplyDamageTorque(Subsystem *gyro, Scalar x, Scalar y, Scalar z, Scalar magnitude)
|
||
{
|
||
if (gyro != 0)
|
||
static_cast<Gyroscope *>(gyro)->ApplyDamageTorque(x, y, z, magnitude);
|
||
}
|
||
```
|
||
|
||
## 2. Port call sites
|
||
|
||
### (a) Weapon-damage path — `mech.cpp` TakeDamageMessageHandler
|
||
- **extern** at `mech.cpp:219` (next to the existing gyro externs :217-218): `extern void GyroApplyDamage(Subsystem *, const Damage &);`
|
||
- **insert at mech.cpp:~596** — after the `Check(message)`/MP-diag block, BEFORE the `lastInflictingID` write at :602 (binary order: gyro @4a0264 precedes the EntityID assign @4a0327, the threat feed @4a039c and zone resolution @4a037a):
|
||
```cpp
|
||
// Binary @0x4a0264-0x4a0300 (hub FUN_004a0230): cockpit hit-BOUNCE. Feed
|
||
// the gyro the raw Damage record FIRST -- before inflictor bookkeeping,
|
||
// threat feed and zone resolution (an invalid-zone hit still bounces).
|
||
// Sole gate: non-null gyro (mech+0x528). The fan-out itself no-ops
|
||
// CollisionDamageType(0) and damageAmount==0 (@4b298c/@4b299e); on a
|
||
// replicant the binary only WARNS ("Replicant Mech recieving
|
||
// takedamagemessage!", MECH.CPP:986) and proceeds -- no replicant gate.
|
||
if (gyroSubsystem != 0)
|
||
GyroApplyDamage(gyroSubsystem, message->damageData);
|
||
```
|
||
This covers aimed AND unaimed hits and the collision TakeDamage messages (type 0 correctly no-ops inside).
|
||
|
||
### (b) Crushable-icon crunch — `mech4.cpp:3310` (replace the dormant comment inside the `dmg.damageAmount == 0.00123f` branch, after `dmg.damageAmount = 0.0f;` at :3309, before the GroundLog)
|
||
```cpp
|
||
// Binary @4aa7ce-4aa871: the gyro CRUNCH. n = Normalize(dmg.damageForce)
|
||
// (FUN_004087f4 @4aa7ea; damageForce = the delta-v across the bounce,
|
||
// written by engine Mover::ProcessCollisionList, MOVER.cpp:1299 -- no
|
||
// new plumbing), then torque along n @ 0.4 (@4aa81e) + an upward
|
||
// impulse @ 0.2 (@4aa86c). Normalize is UNGUARDED in the binary too.
|
||
if (gyroSubsystem != 0)
|
||
{
|
||
Vector3D n;
|
||
n.Normalize(dmg.damageForce);
|
||
GyroApplyDamageTorque (gyroSubsystem, n.x, n.y, n.z, 0.4f); // @4aa81e 0x3ecccccd
|
||
GyroApplyDamageImpulse(gyroSubsystem, 0.0f, 1.0f, 0.0f, 0.2f); // @4aa86c 0x3e4ccccd
|
||
}
|
||
```
|
||
externs near mech4.cpp's existing gyro extern (:3166 style, or file-scope): `extern void GyroApplyDamageImpulse(Subsystem *, Scalar, Scalar, Scalar, Scalar);` / `...GyroApplyDamageTorque(...)`. **Do NOT add any kick to the blocking-hit branch (mech4.cpp:3315+)** — verified no gyro calls in @4aa88a-4aab5f.
|
||
|
||
### (c) Alternate-gait engage jolt + engaged-gait rumble — `mech4.cpp`, immediately before the channel-advance pair at :2056-2058 (`adv = AdvanceBodyAnimation(dt, 1); legAdv = AdvanceLegAnimation(dt);` — the binary runs @4aa158-4aa365 directly before its IsDisabled + AdvanceLegAnimation sequence @4aa365-4aa399):
|
||
```cpp
|
||
// ===== Binary @4aa158-4aa2be: alternate-gait toggle + ENGAGE JOLT =====
|
||
// [T3 NAMING: the gate is (cycling && speedDemand < 0) -- by the port's
|
||
// own sign convention 0x3f4 reads as a REVERSE-gait select; the
|
||
// airborneSelect/airborneCycleRate names are kept but suspect.]
|
||
if (airborneCycleRate != -1.1f) // sentinel @0x4ab174 (0xbf8ccccd)
|
||
{
|
||
if (legCycleSpeed > 0.0f // @4aa16d
|
||
&& s_realControls && controlsMapper != 0
|
||
&& controlsMapper->speedDemand < 0.0f) // @4aa182
|
||
{
|
||
if (airborneSelect == 0) // rising edge @4aa197
|
||
{
|
||
airborneSelect = 1;
|
||
RequestActionFlags(0x100); // @4aa1b5 FUN_004a4c54
|
||
forwardCycleRate = airborneCycleRate; // @4aa1bd
|
||
if (gyroSubsystem != 0) // @4aa1c9
|
||
{
|
||
Scalar r = RandomUnit(); // @4aa1dc value roll
|
||
if (RandomUnit() > 0.5f) // @4aa1f6 sign roll (STRICT >, 0.5 @0x4ab170)
|
||
r = -r;
|
||
// dir = (0,0,r): Z-axis, random magnitude+sign; SAME vec both calls
|
||
GyroApplyDamageTorque (gyroSubsystem, 0.0f, 0.0f, r, 0.4f); // @4aa254
|
||
GyroApplyDamageImpulse(gyroSubsystem, 0.0f, 0.0f, r, 0.4f); // @4aa288
|
||
}
|
||
}
|
||
}
|
||
else if (airborneSelect != 0) // falling edge @4aa292
|
||
{
|
||
airborneSelect = 0;
|
||
RequestActionFlags(0x100);
|
||
forwardCycleRate = groundCycleRate; // @4aa2b2
|
||
}
|
||
}
|
||
// ===== Binary @4aa2be-4aa365: engaged-gait RUMBLE (0.4s period) =====
|
||
if (airborneSelect != 0
|
||
&& legCycleSpeed > walkStrideLength // @4aa2cc vs @0x534
|
||
&& gyroSubsystem != 0)
|
||
{
|
||
if (gyroRumbleTimer > 0.0f) // @4aa2eb
|
||
gyroRumbleTimer -= dt; // @4aa356 fsubr
|
||
else
|
||
{
|
||
GyroApplyDamageImpulse(gyroSubsystem, 0.0f, 1.0f, 0.0f, 0.2f); // @4aa342 up, 0x3e4ccccd
|
||
gyroRumbleTimer = 0.4f; // @4aa347 0x3ecccccd
|
||
}
|
||
}
|
||
```
|
||
Note: this block is also the port's first authentic WRITER of `airborneSelect` (previously never set). All gate members exist in mech.hpp (:641-657, :695).
|
||
|
||
### (d) Firing recoil kick — `projweap.cpp:603` `ProjectileWeapon::FireWeapon`, insert after `Check(this)` at :605, BEFORE `ConsumeRound()` at :608 (binary: heat add @4bc11e → kick @4bc136-4bc199 → tracer/spawn; no early-out precedes the kick):
|
||
```cpp
|
||
// Binary @4bc136-4bc19c: firing RECOIL kick, before the ammo pull. Gate:
|
||
// per-shot damage > 3.0f (@0x4bc3f4) && owner mech has a gyro. Direction
|
||
// (0, 0.6, -1.5) (0x3f19999a/0xbfc00000), magnitude = damageAmount/16
|
||
// (@0x4bc3f8). (The binary's preceding heat add [+0x3d8]->[+0x1c8] gated
|
||
// by HeatModelActive @4ad7d4 is a separate pre-existing gap -- not this hook.)
|
||
if (damageData.damageAmount > 3.0f && owner != 0)
|
||
{
|
||
extern Subsystem *BTMechGyro(void *mech);
|
||
extern void GyroApplyDamageImpulse(Subsystem *, Scalar, Scalar, Scalar, Scalar);
|
||
Subsystem *g = BTMechGyro(owner); // mech+0x528 via bridge
|
||
if (g != 0)
|
||
GyroApplyDamageImpulse(g, 0.0f, 0.6f, -1.5f,
|
||
damageData.damageAmount * 0.0625f);
|
||
}
|
||
```
|
||
New accessor in **mech.cpp** (projweap.cpp does not include mech.hpp — databinding rule):
|
||
```cpp
|
||
// Gyro accessor for TUs without a complete Mech (projweap.cpp recoil @4bc194).
|
||
Subsystem *BTMechGyro(void *mech)
|
||
{
|
||
return mech != 0 ? ((Mech *)mech)->gyroSubsystem : 0;
|
||
}
|
||
```
|
||
**Do NOT touch `MissileLauncher::FireWeapon`** (mislanch.cpp:217 — its own override, does not chain to @4bc104; binary-verified no gyro kick @4bcc60).
|
||
|
||
## 3. Argument plumbing (what each site supplies today / gaps)
|
||
|
||
| Site | type | amount | direction | burstCount | status |
|
||
|---|---|---|---|---|---|
|
||
| mech.cpp handler | `message->damageData.damageType` | `.damageAmount` | `.damageForce` | `.burstCount` | fields all exist (ENTITY3.h `damageData` @msg+0x2C) |
|
||
| crushable crunch | n/a (direct kicks) | fixed 0.4/0.2 | `dmg.damageForce` = Δv from `ProcessCollisionList` (mech4.cpp:3301 → MOVER.cpp:1299) | n/a | zero new plumbing |
|
||
| gait jolt/rumble | n/a | fixed 0.4/0.2 | (0,0,±rand) / (0,1,0) | n/a | zero new plumbing |
|
||
| recoil | n/a | `damageData.damageAmount * 0.0625f` (mechweap.hpp:326 @0x3A8) | fixed (0,0.6,−1.5) | n/a | zero new plumbing |
|
||
|
||
**GAP (flag, not a blocker):** our current weapon senders — beam @mech4.cpp:3029-3040 (`kShotDamage`, type Explosive), projectile impact @mech4.cpp:871-878, collision @mech4.cpp:3477 — never set `Damage::damageForce`; the `Damage()` ctor zeroes it (DAMAGE.cpp:38), so every hit takes the fan-out's authentic random-horizontal fallback (@4b29d7). Works and is binary-legal, but for directional fidelity the beam/projectile senders should later fill `dmg.damageForce` with the world flight direction (beam: `impact - muzzle`; projectile: `p.vel`). Also note both senders currently mark beam hits `ExplosiveDamageType` (so `burstCount=1` multiplies benignly); when weapon types are made authentic (Laser/Ballistic), the fan-out picks the matching response row automatically. Second GAP already noted in (d): the FireWeapon heat add is missing (pre-existing, separate).
|
||
|
||
## 4. Verification checklist (one run, `BT_GYRO_LOG=1`)
|
||
|
||
1. **Build**: `static_assert` locks compile (0x390 vec, AffineMatrix/ReconMatrix sizes); grep the link log for new unresolved externals (`/FORCE` hides them — `GyroApplyDamage*`, `BTMechGyro` must resolve).
|
||
2. **Hit bounce (message path)**: fire at the player mech (or use the enemy shooting back). In `btl4.log` expect, in order, on each hit: the existing BT_GYRO_LOG frame lines showing `eyeForce` spike to nonzero right after the hit → `eyePosition` oscillation over the next ~1s (spring integrator) → decay back to (0,0,0); and `bodyForce`/`bodyOrientation` doing the same (torque + vertical). Add a one-line log in `ApplyDamageResponse` while verifying: `[gyro-dmg] type= amount= burst= dir=(..) t/p/y/v=..` and confirm t/p/y/v are ≤ 1.3 and Explosive scales with burstCount.
|
||
3. **Visible jolt**: inside view (cockpit) — the eyepoint/canopy visibly kicks on every laser/missile hit and settles; direction varies (random fallback) run to run.
|
||
4. **Collision correctness**: walk into a wall — NO message-path bounce (type 0 early-out; log shows the fan-out entry absent), but walking through a crushable icon logs `[ground] CRUNCH` AND a gyro kick (torque along Δv + 0.2 up).
|
||
5. **Recoil**: fire a projectile weapon with damage > 3 — a small forward-down kick per shot (magnitude = damage/16); missiles produce none.
|
||
6. **Rumble**: engage the reverse/alternate gait (throttle reverse while cycling) — one Z jolt on the edge, then a 0.4s-period up-shake while `legCycleSpeed > walkStrideLength`; disengage restores `forwardCycleRate = groundCycleRate`.
|
||
7. **Regression**: normal combat run un-regressed (no NaN in joints — the eye/mech joint writers already guard); `-net` smoke: replicant-received damage on the master still bounces (binary warns only).
|
||
8. **KB**: record the fan-out semantics + the 0x390 vibrationDirection correction and the 0x5c4 re-type in `context/decomp-reference.md` / the gyro section of the task-#56 topic, tiers [T1].
|
||
|
||
Files touched: `game/reconstructed/gyro.hpp`, `gyro.cpp`, `mech.hpp`, `mech.cpp`, `mech3.cpp` (one line), `mech4.cpp`, `projweap.cpp`. Scratch tooling: `C:\git\bt411\scratchpad\dis_4b2980.py`, `C:\git\bt411\scratchpad\dis_range.py`. |