From 4704ab3d415c96a398b5805548d113a9e3a8059d Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Fri, 31 Jul 2026 10:41:39 -0500 Subject: [PATCH] #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 Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J --- context/combat-damage.md | 17 +++++ game/reconstructed/ammobin.cpp | 11 +++ game/reconstructed/ammobin.hpp | 6 ++ game/reconstructed/mech4.cpp | 130 +++++++++++++++++++++++++++++++- game/reconstructed/mislanch.cpp | 34 ++++++++- game/reconstructed/projweap.cpp | 3 +- 6 files changed, 196 insertions(+), 5 deletions(-) diff --git a/context/combat-damage.md b/context/combat-damage.md index 91debbc..d345549 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -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**, diff --git a/game/reconstructed/ammobin.cpp b/game/reconstructed/ammobin.cpp index 8182996..d72421b 100644 --- a/game/reconstructed/ammobin.cpp +++ b/game/reconstructed/ammobin.cpp @@ -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; +} diff --git a/game/reconstructed/ammobin.hpp b/game/reconstructed/ammobin.hpp index 781a359..84657a4 100644 --- a/game/reconstructed/ammobin.hpp +++ b/game/reconstructed/ammobin.hpp @@ -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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 8c97e24..867fd28 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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; diff --git a/game/reconstructed/mislanch.cpp b/game/reconstructed/mislanch.cpp index db3742e..3bc00b7 100644 --- a/game/reconstructed/mislanch.cpp +++ b/game/reconstructed/mislanch.cpp @@ -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 diff --git a/game/reconstructed/projweap.cpp b/game/reconstructed/projweap.cpp index 7b54f67..bcbe2da 100644 --- a/game/reconstructed/projweap.cpp +++ b/game/reconstructed/projweap.cpp @@ -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) //#############################################################################