Missiles (task #67): launch through the WEAPON MOUNT frame -- the

backward-firing rack

User report (madcat): missiles sometimes leave the mech BACKWARD; lasers
always fine.  Root cause: only missiles fly the authored MuzzleVelocity
vector, and BTPushProjectile rotated it through the mech BODY basis
(localToWorld) -- but the madcat's racks ride the TORSO.  The muzzle POINT
was segment-resolved (tracked the twist) while the launch DIRECTION followed
the LEGS: twist the torso far enough and rounds left the back.  Lasers/ACs
aim straight at the designated pick (no launch vector) -- unaffected, exactly
as observed.

Binary ground truth [T1]: the fire builder FUN_004bcc60 spawns the missile
with the FULL muzzle-segment frame (the real GetMuzzlePoint fills an
AffineMatrix into the descriptor, :8762-8764), composes the authored MV with
the z-NEGATION (:8759-8761 -- confirming the 2026-07-12 telemetry finding)
onto the mech's own localVelocity (FUN_004b9cbc = owner+0x1c4), and the dumb
seeker re-aims 100u ahead of the MISSILE's own frame each frame (0x4be9a0
disasm) -- the mount orientation IS the launch direction.

Port: BTPushProjectile takes muzzle_seg and rotates the launch vector through
the mount segment's world frame (segment-to-entity x localToWorld, the same
matrix the muzzle point already used), keeping the z-negation convention, and
inherits the shooter's world velocity; body-basis fallback for callers
without a segment.  All four call sites pass GetSegmentIndex().

Bonus decode banked for the full Missile-entity revival (KB-worthy): the
three authentic performances -- Seeker 0x4be9a0 (aim = target-frame offset /
100u-ahead dumb + loft/lead 0x4beae4), Thruster 0x4be474 (quaternion-slerp
BODY TURN toward the aim at turnRate deg/s), MoveAndCollide 0x4bef78
(velocity aerodynamically aligned to thrust via signed-square per-axis gains,
ballistic droop as fuel burns, PROXIMITY FUSE on seeker rangeToTarget,
max-range + altitude-floor retire).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-16 16:46:56 -05:00
co-authored by Claude Opus 4.8
parent c51d64a6b8
commit ab6ea83e3b
3 changed files with 48 additions and 15 deletions
+40 -10
View File
@@ -1075,7 +1075,7 @@ void
void
BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos,
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided,
int weapon_subsys, int splash_burst)
int weapon_subsys, int splash_burst, int muzzle_seg)
{
// MUZZLE (muzzle wave, 2026-07-12): the passed muzzle is now the weapon's
// AUTHENTIC mount segment (GetMuzzlePoint reads the real segmentIndex --
@@ -1137,20 +1137,50 @@ void
if (launch_velocity != 0 && shooter != 0)
{
// FRAME CONVENTION (telemetry-verified 2026-07-12): the authored
// MuzzleVelocity is (0,0,+100) -- the launcher frame's +Z is
// FORWARD, but the mech's body frame faces -Z. Transforming +Z
// through the body basis launched every round BACKWARD (then the
// seeker looped it through the ground onto the target --
// user-reported). Map launcher-forward (+Z) onto body-forward
// (-Z basis): negate the z term.
// MuzzleVelocity is (0,0,+100) with a NEGATED z when composed --
// the binary's own fire builder does `local_40 -= MV.z`
// (FUN_004bcc60 @part_013.c:8761) [T1].
// TORSO-TWIST FIX (task #67, user-reported "missiles fire out of
// his back"): rotate through the WEAPON MOUNT SEGMENT's world
// frame, not the body basis. The MadCat's racks ride the TORSO:
// the muzzle POINT tracked the twist (segment-resolved) while the
// launch DIRECTION followed the LEGS -- twist far enough and
// rounds left backward. The binary spawns the missile with the
// full muzzle-segment frame (GetMuzzlePoint fills an AffineMatrix
// into the spawn descriptor, @0x4bcc60:8762-8764) and the dumb
// seeker aims 100u ahead of the MISSILE's own frame -- segment
// orientation IS the authentic launch direction.
Mech *sm = (Mech *)shooter;
UnitVector ax, ay, az;
sm->localToWorld.GetFromAxis(X_Axis, &ax);
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
sm->localToWorld.GetFromAxis(Z_Axis, &az);
int haveFrame = 0;
if (muzzle_seg >= 0)
{
EntitySegment *seg = sm->GetSegment(muzzle_seg);
if (seg != 0)
{
AffineMatrix mw;
mw.Multiply(seg->GetSegmentToEntity(), sm->localToWorld);
mw.GetFromAxis(X_Axis, &ax);
mw.GetFromAxis(Y_Axis, &ay);
mw.GetFromAxis(Z_Axis, &az);
haveFrame = 1;
}
}
if (!haveFrame)
{
sm->localToWorld.GetFromAxis(X_Axis, &ax);
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
sm->localToWorld.GetFromAxis(Z_Axis, &az);
}
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y - az.x*launch_velocity->z;
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y - az.y*launch_velocity->z;
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y - az.z*launch_velocity->z;
// (task #67) inherit the shooter's motion: the binary adds the
// mech's localVelocity into the spawn velocity (FUN_004b9cbc =
// owner+0x1c4 read, composed @0x4bcc60:8758-8761) [T1].
p.vel.x += sm->worldLinearVelocity.x;
p.vel.y += sm->worldLinearVelocity.y;
p.vel.z += sm->worldLinearVelocity.z;
Scalar lv = (Scalar)sqrtf(p.vel.x*p.vel.x + p.vel.y*p.vel.y + p.vel.z*p.vel.z);
if (lv > 1.0f) { p.speed = lv; haveLaunch = 1; }
}
+5 -3
View File
@@ -73,7 +73,7 @@
extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target,
const Point3D &targetPos, Scalar speed, Scalar damage,
const Vector3D *launch_velocity = 0, int guided = 1,
int weapon_subsys = -1, int splash_burst = 0);
int weapon_subsys = -1, int splash_burst = 0, int muzzle_seg = -1);
//###########################################################################
// Port-side salvo replication state (missile-visibility wave).
@@ -317,7 +317,8 @@ void MissileLauncher::FireWeapon()
(i == 0) ? damageData.damageAmount : 0.0f,
&launchVelocity, 1,
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
(i == 0) ? nmiss : 0 /*salvo-lead: cluster splash baseBurst*/);
(i == 0) ? nmiss : 0 /*salvo-lead: cluster splash baseBurst*/,
GetSegmentIndex() /*task #67: launch through the MOUNT frame (torso twist)*/);
// Salvo replication (missile-visibility wave): bump the fire counter + stamp
// the aim point; the extended update record carries both to peer nodes.
@@ -389,7 +390,8 @@ void
for (int i = 0; i < n; ++i)
BTPushProjectile(mz, owner, 0 /*no entity: aim point only*/,
rec->salvoTarget, spd, 0.0f /*VISUAL*/, &launchVelocity, 1,
subsystemID /*per-round detonation resolve on this node*/);
subsystemID /*per-round detonation resolve on this node*/,
0, GetSegmentIndex() /*task #67 mount frame*/);
if (getenv("BT_PROJ_LOG"))
DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n
<< " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y
+3 -2
View File
@@ -95,7 +95,7 @@
extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target,
const Point3D &targetPos, Scalar speed, Scalar damage,
const Vector3D *launch_velocity = 0, int guided = 1,
int weapon_subsys = -1, int splash_burst = 0); // AC: no cluster splash (default 0)
int weapon_subsys = -1, int splash_burst = 0, int muzzle_seg = -1); // AC: no cluster splash (default 0)
//#############################################################################
@@ -846,7 +846,8 @@ void
|| rec->fireTarget.z != 0.0f);
BTPushProjectile(mz, owner, 0 /*aim point only, no entity*/, rec->fireTarget,
spd, 0.0f /*VISUAL*/,
haveAim ? 0 : &launchVelocity, 0 /*ballistic*/, subsystemID);
haveAim ? 0 : &launchVelocity, 0 /*ballistic*/, subsystemID,
0, GetSegmentIndex() /*task #67 mount frame*/);
const char *mzf = getenv("BT_MUZZLE"); // default ON; =0 disables
if (mzf == 0 || mzf[0] != '0')