#84: missiles ACCELERATE like the binary -- the pool flew at constant

rack-eject speed; the authored MissileThruster was never applied

The binary Missile hosts a MissileThruster subsystem: authored
acceleration for BurnTime seconds after the MuzzleVelocity eject.
Authored values decoded from BTL4.RES (type-15 missile model records,
reached from the AmmoBin's ammoModelFile via the type-1 MODELLIST
indirection): SRM 2.5s @ 600 u/s^2, LRM 10s @ 300 (climb 50), Streak
3s @ 300 (turn 360deg/s), NARC 10s @ 300; splash 30 all (port constant
was already right); authored drag ~0.001 = negligible.

The port pool flew every round at CONSTANT |MuzzleVelocity| -- SRMs 100
u/s, LRMs 30 u/s (10x slower than authored) -- the entirety of the
night-7 "missiles are very slow" report, and the driver of the
"explosion before the missiles arrive" perception (the salvo-lead
detonates while the slow spread rounds straggle in).

Fix: BTMissileThrustOf (mech4.cpp) lazily parses + caches the RES
thruster table (ModelList ids aliased to their type-15 member's
burn/accel); both launch paths (master FireWeapon + the replicant salvo
mirror -- peers see the same speed) resolve through the launcher's bin
and pass burn/accel into the pool; the advance integrates speed +=
accel*dt while burn remains, heading preserved.  Ballistic rounds
(autocannon/gauss) pass 0/0 -- unchanged.  Impact log now prints
v=/burnLeft= evidence.

Verified live (solo, BT_SPAWN_ENEMY + BT_AF_MISSILE): thrust table 8
entries cached; Black Hawk Streak resolve burn=3 accel=300; impacts at
v=277 u/s (was a constant 100).  Fun authenticity note: the Black
Hawk's "SRM6" fires strk (STREAK) ammo per its authored bin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
This commit is contained in:
Joe DiPrima
2026-07-31 10:41:39 -05:00
co-authored by Claude Fable 5
parent ac7c46b87a
commit 4704ab3d41
6 changed files with 196 additions and 5 deletions
+17
View File
@@ -359,6 +359,23 @@ Band effects: `MechDeathHandler::Tick` fires the CURRENT band descriptor on any
(the binary's changed-flag semantics, workflow-verified) — a damaged mech under fire smokes/
burns per hit; the earlier crossing-only gate was over-tight (the metronome was SHKWAVE).
## Missile THRUST -- FIXED (2026-07-31, issue #84) [T1 values / T2 verified]
The binary Missile hosts a **MissileThruster** subsystem: the round leaves the rack at the
launcher's authored MuzzleVelocity (SRM eject 100 u/s flat; LRM 30 u/s up-tilted) and then
ACCELERATES while BurnTime remains. Authored values (BTL4.RES type-15 missile model records
+0x44/+0x48, reached from the AmmoBin's `ammoModelFile` through the type-1 MODELLIST
indirection): **SRM 2.5s @ 600 u/s² (turn 120°/s) · LRM 10s @ 300 (turn 60°/s, climb 50) ·
Streak 3s @ 300 (turn 360°/s) · NARC 10s @ 300**; SplashRadius 30 for all (the port's constant
was right); authored drag ~0.001 = negligible. The port pool flew at CONSTANT eject speed --
LRMs 10× slower than authored -- which was the whole of the field "missiles are slow" report,
and the "explosion before the missiles arrive" perception (the salvo-lead detonates while the
slow spread rounds straggle). Fixed: `BTMissileThrustOf` (mech4.cpp) lazily caches the RES
thruster table (ModelList ids aliased to their type-15 member); both launch paths (master fire
+ the replicant salvo mirror) pass burn/accel into the pool, whose advance integrates
`speed += accel·dt` while burning. Verified live: Streak impacts at 277 u/s (was 100).
Note: the Black Hawk's "SRM6" fires **strk** (Streak) ammo per its bin -- the ammo model, not
the launcher name, decides the flight profile.
## Ballistic damage type -- FIXED (2026-07-23, issue #27) [T2]
Playtest matchlog forensics (2,591 applied-damage events over 2 rounds): the damageType
histogram had Collision/Explosive/Laser/Energy but **Ballistic (type 1) NEVER appeared**,
+11
View File
@@ -631,3 +631,14 @@ Subsystem *CreateAmmoBinSubsystem(Mech *owner, int id, void *seg)
return (Subsystem *) new (Memory::Allocate(0x22c))
AmmoBin(owner, id, (AmmoBin::SubsystemResource *)seg);
}
//###########################################################################
// #84 bridge -- the launcher resolves its round's MODEL RESOURCE ID through
// its connected bin (the binary Missile ctor receives the same id via the
// launch descriptor). Complete-AmmoBin TU accessor.
//###########################################################################
int BTAmmoBinModelFile(void *bin)
{
return (bin != 0) ? ((AmmoBin *)bin)->AmmoModelFileID() : -1;
}
+6
View File
@@ -260,6 +260,12 @@
// member. The 0x54 ammoAlarm (was an 8-byte HeatAlarm) lands ammoModelFile
// at 0x1E8 -- exact binary layout, locked by AmmoBinLayoutCheck.
int ammoModelFile; // @0x1E8 (word 0x7A) round model index
public:
// #84: the round's model RESOURCE ID (the type-15 record carrying the
// MissileThruster tail the binary Missile ctor copies) -- read by the
// launcher's fire path via the BTAmmoBinModelFile bridge.
int AmmoModelFileID() const { return ammoModelFile; }
protected:
int explosionModelFile; // @0x1EC (word 0x7B) cook-off explosion model index
// @0x1F0..0x21C (words 0x7C..0x87): the cook-off DAMAGE record -- a real
+129 -1
View File
@@ -843,6 +843,8 @@ struct BTProjectile {
// (matches the ONE arcade cluster missile). 0 on every
// other round (a straight tracer / non-lead volley round)
// so splash is NOT re-applied + floored-at-1 per round.
Scalar accel; // #84: authored ThrusterAcceleration (u/s^2; 0 = unpowered round)
Scalar burnLeft; // #84: authored BurnTime remaining (s); accel applies while > 0
int active;
};
static BTProjectile gProjectiles[64];
@@ -882,6 +884,105 @@ void BTProjectilesClearAll(void)
gProjectiles[i].active = 0;
}
//###########################################################################
// #84: the missile-model thruster table (BTL4.RES type-15 "GameModel" 0x54
// records; the same records the binary's Missile ctor copies +0x40..0x4C
// from). The launcher's AmmoBin carries the model's RESOURCE ID
// (ammoModelFile); resolve burn/accel from the RES directory, parsed once
// and cached. Reading the shipped RES directly mirrors the binary's stream
// (the engine-side in-memory path has no raw-record accessor in the port).
//###########################################################################
int BTMissileThrustOf(int model_id, float *out_burn, float *out_accel)
{
enum { kMax = 32 };
static int s_loaded = 0, s_n = 0;
static int s_id[kMax];
static float s_burn[kMax], s_accel[kMax];
if (!s_loaded)
{
s_loaded = 1;
FILE *f = fopen("BTL4.RES", "rb"); // cwd == content\ at runtime
if (f != 0)
{
int hdr[3];
if (fread(hdr, 4, 3, f) == 3 && hdr[2] > 0 && hdr[2] < 65536)
{
int maxID = hdr[2];
unsigned *dir = (unsigned *)malloc(maxID * 4);
if (dir != 0 && (int)fread(dir, 4, maxID, f) == maxID)
{
// pass 1: the type-15 GameModel 0x54 records (the missile models)
for (int i = 0; i < maxID && s_n < kMax; ++i)
{
if (dir[i] == 0) continue;
int rh[2]; // rid, rtype
unsigned meta[4]; // prio, flags, off, len
if (fseek(f, dir[i], SEEK_SET) != 0) continue;
if (fread(rh, 4, 2, f) != 2) continue;
if (rh[1] != 15) continue; // GameModel records only
if (fseek(f, dir[i] + 40, SEEK_SET) != 0) continue;
if (fread(meta, 4, 4, f) != 4) continue;
if (meta[3] != 0x54) continue; // the missile model record
float tail[5]; // +0x40: rot,burn,accel,climb,splash
if (fseek(f, meta[2] + 0x40, SEEK_SET) != 0) continue;
if (fread(tail, 4, 5, f) != 5) continue;
s_id[s_n] = rh[0];
s_burn[s_n] = tail[1];
s_accel[s_n] = tail[2];
++s_n;
}
// pass 2: the AmmoBin's ammoModelFile references the TYPE-1
// MODELLIST ('srm'/'lrm'/'strk'/'nrk'), which redirects to the
// model family (type 10/3/15/18 members). Alias each list id
// to its type-15 member's thruster values.
const int nModels = s_n;
for (int i = 0; i < maxID && s_n < kMax; ++i)
{
if (dir[i] == 0) continue;
int rh[2];
unsigned meta[4];
if (fseek(f, dir[i], SEEK_SET) != 0) continue;
if (fread(rh, 4, 2, f) != 2) continue;
if (rh[1] != 1) continue; // ModelList only
if (fseek(f, dir[i] + 40, SEEK_SET) != 0) continue;
if (fread(meta, 4, 4, f) != 4) continue;
int lst[9]; // count + up to 8 member ids
if (meta[3] < 8 || meta[3] > 36) continue;
if (fseek(f, meta[2], SEEK_SET) != 0) continue;
int cnt = (int)(meta[3] / 4) - 1;
if (fread(lst, 4, cnt + 1, f) != (size_t)(cnt + 1)) continue;
if (lst[0] < 1 || lst[0] < cnt) cnt = lst[0] < cnt ? lst[0] : cnt;
for (int m = 1; m <= cnt; ++m)
for (int k = 0; k < nModels; ++k)
if (s_id[k] == lst[m])
{
s_id[s_n] = rh[0]; // the LIST id aliases the model
s_burn[s_n] = s_burn[k];
s_accel[s_n] = s_accel[k];
++s_n;
m = cnt + 1; // one alias per list
break;
}
}
}
if (dir != 0) free(dir);
}
fclose(f);
DEBUG_STREAM << "[projectile] thrust table: " << s_n
<< " missile model(s) cached from BTL4.RES\n" << std::flush;
}
}
for (int i = 0; i < s_n; ++i)
{
if (s_id[i] == model_id)
{
*out_burn = s_burn[i]; *out_accel = s_accel[i];
return 1;
}
}
return 0;
}
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
//###########################################################################
@@ -1207,7 +1308,8 @@ 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 muzzle_seg, int damage_type)
int weapon_subsys, int splash_burst, int muzzle_seg, int damage_type,
Scalar thrust_accel, Scalar thrust_burn)
{
// MUZZLE (muzzle wave, 2026-07-12): the passed muzzle is now the weapon's
// AUTHENTIC mount segment (GetMuzzlePoint reads the real segmentIndex --
@@ -1336,6 +1438,8 @@ void
p.shooter = (Entity *)shooter;
p.weaponSubsys = weapon_subsys;
p.splashBurst = splash_burst; // >0 only on a salvo-lead round
p.accel = (thrust_accel > 0.0f) ? thrust_accel : 0.0f; // #84 thruster
p.burnLeft = (thrust_burn > 0.0f) ? thrust_burn : 0.0f;
// RACK-TUBE SPREAD [T3, physically grounded]: the binary's Missile
// entities each launch from their own rack tube (per-tube authored
@@ -1435,6 +1539,29 @@ static void
}
}
// #84 THRUSTER BURN: the binary Missile hosts a MissileThruster whose
// authored acceleration drives the round while BurnTime remains
// (MISTHRST: acceleration = (0,0,-thrusterAccel) in the missile frame,
// integrated by Missile::MoveAndCollide @4bef78). The pool flew at
// CONSTANT |MuzzleVelocity| -- the rack-eject speed (SRM 100, LRM 30
// u/s) -- which is why field missiles crawled (Oracle/Rajel, #84).
// Authored (BTL4.RES type-15 missile models, +0x44/+0x48): SRM
// burn 2.5s @ 600 u/s^2, LRM 10s @ 300, Streak 3s @ 300. Drag is
// authored ~0.001 (negligible at combat ranges) -- integrate accel
// along the current heading, un-damped, range-capped as before. [T1
// values / T2 integration]
if (p.burnLeft > 0.0f && p.accel > 0.0f)
{
Scalar burn_dt = (dt < p.burnLeft) ? dt : p.burnLeft;
p.burnLeft -= burn_dt;
Scalar ns = p.speed + p.accel * burn_dt;
if (p.speed > 0.01f)
{
Scalar k = ns / p.speed;
p.vel.x *= k; p.vel.y *= k; p.vel.z *= k;
}
p.speed = ns;
}
p.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt; p.pos.z += p.vel.z*dt;
p.traveled += p.speed * dt;
@@ -1590,6 +1717,7 @@ static void
DEBUG_STREAM << "[projectile] IMPACT damage=" << p.damage
<< " subsys=" << p.weaponSubsys
<< " v=" << p.speed << " burnLeft=" << p.burnLeft // #84 thrust evidence
<< (mgr ? " (msgmgr bundled)" : " (direct)")
<< " (zone cyl-resolved)\n" << std::flush;
+31 -3
View File
@@ -74,7 +74,14 @@ 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 muzzle_seg = -1,
int damage_type = 2 /*ExplosiveDamageType default*/);
int damage_type = 2 /*ExplosiveDamageType default*/,
Scalar thrust_accel = 0.0f, Scalar thrust_burn = 0.0f /*#84 thruster*/);
// #84: resolve this launcher's authored thruster (burn s, accel u/s^2) through
// its connected bin's round-model resource. 0/0 when unresolvable (flies at
// the old constant eject speed -- never worse than before).
extern void *BTWeaponAmmoBin(void *weapon); // projweap.cpp
extern int BTAmmoBinModelFile(void *bin); // ammobin.cpp
extern int BTMissileThrustOf(int model_id, float *out_burn, float *out_accel); // mech4.cpp
//###########################################################################
// Port-side salvo replication state (missile-visibility wave).
@@ -305,6 +312,18 @@ void MissileLauncher::FireWeapon()
// MuzzleVelocity VECTOR rides along (missile-arc wave): the round leaves the rack
// on the authored up-tilt, then the seeker loft + steering arc it onto the target.
int nmiss = (missileCount > 0 && missileCount < 40) ? missileCount : 1;
float tBurn = 0.0f, tAccel = 0.0f; // #84 authored thruster
{
void *bin = BTWeaponAmmoBin(this);
int mid = (bin != 0) ? BTAmmoBinModelFile(bin) : -1;
int hit = 0;
if (mid >= 0)
hit = BTMissileThrustOf(mid, &tBurn, &tAccel);
if (getenv("BT_PROJ_LOG"))
DEBUG_STREAM << "[projectile] thrust resolve: bin=" << bin
<< " modelID=" << mid << " hit=" << hit
<< " burn=" << tBurn << " accel=" << tAccel << "\n" << std::flush;
}
for (int i = 0; i < nmiss; ++i)
// DAMAGE + SPLASH ONCE PER SALVO (task #62 fix): the arcade fires ONE
// cluster Missile per trigger, and its zone damage is applied EXACTLY ONCE
@@ -320,7 +339,8 @@ void MissileLauncher::FireWeapon()
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
(i == 0) ? nmiss : 0 /*salvo-lead: cluster splash baseBurst*/,
GetSegmentIndex() /*task #67: launch through the MOUNT frame (torso twist)*/,
(int)damageData.damageType /*missile = Explosive (authentic)*/);
(int)damageData.damageType /*missile = Explosive (authentic)*/,
tAccel, tBurn /*#84: the round ACCELERATES like the binary Missile*/);
// Salvo replication (missile-visibility wave): bump the fire counter + stamp
// the aim point; the extended update record carries both to peer nodes.
@@ -386,11 +406,19 @@ void
int n = rec->salvoRounds;
if (n < 1 || n > 40)
n = (missileCount > 0 && missileCount < 40) ? missileCount : 1;
float tBurn = 0.0f, tAccel = 0.0f; // #84: mirror the master's
{ // thruster so peer missiles
void *bin = BTWeaponAmmoBin(this); // fly at the same speed
int mid = (bin != 0) ? BTAmmoBinModelFile(bin) : -1;
if (mid >= 0)
BTMissileThrustOf(mid, &tBurn, &tAccel);
}
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*/,
0, GetSegmentIndex() /*task #67 mount frame*/);
0, GetSegmentIndex() /*task #67 mount frame*/,
2 /*Explosive*/, tAccel, tBurn);
if (getenv("BT_PROJ_LOG"))
DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n
<< " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y
+2 -1
View File
@@ -98,7 +98,8 @@ 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 muzzle_seg = -1,
int damage_type = 2 /*ExplosiveDamageType default*/); // AC: no cluster splash (default 0)
int damage_type = 2 /*ExplosiveDamageType default*/,
Scalar thrust_accel = 0.0f, Scalar thrust_burn = 0.0f); // AC: unpowered (defaults)
//#############################################################################