PFX continuous-rate emission (data-verified) + persistent wreck smoke

The one-burst emitter batching was wrong: in EVERY shipped .PFX,
maxIssue/rate == release_period (DAFC 25/100~=0.2, DNBOOM 35/150~=0.2, DDAM2
35/16~=2, DDTHSMK 30/3=10) -- release_period is the EMISSION WINDOW and rate
is particles/second emitted continuously across it.  The death smoke plume is
authored as a 3/sec trickle for 10 seconds (+6s particle life), not one
same-frame puff.  Emitters now integrate rate*dt until maximum_issue.

Wreck smoke (port addition, [T3]): a destroyed mech re-arms the death/rubble
smoke plume (psfx 1, DDTHSMK) every 10s -- its own authored window -- so the
dead hulk visibly keeps smoking instead of standing there looking alive (the
freeze-death is invisible on an already-stationary target).  New member
Mech::wreckSmokeTimer; UpdateDeathState takes dt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 15:13:10 -05:00
co-authored by Claude Fable 5
parent d9254736ab
commit b44c0f98f6
4 changed files with 43 additions and 14 deletions
+15 -11
View File
@@ -423,7 +423,7 @@ struct BTPfxEmitter
// victim's mech-local space -- e.g. DAFC.PFX sprays -Z, out through the
// front armor toward the shooter). Identity when no frame is known.
D3DXVECTOR3 ax, ay, az;
float sinceRelease;
float emitAccum; // fractional particles owed (rate * dt integration)
int issued;
int active;
};
@@ -534,7 +534,7 @@ void BTStartPfxFrame(int effect_number, float x, float y, float z,
e.ax = xrow ? D3DXVECTOR3(xrow[0], xrow[1], xrow[2]) : D3DXVECTOR3(1, 0, 0);
e.ay = yrow ? D3DXVECTOR3(yrow[0], yrow[1], yrow[2]) : D3DXVECTOR3(0, 1, 0);
e.az = zrow ? D3DXVECTOR3(zrow[0], zrow[1], zrow[2]) : D3DXVECTOR3(0, 0, 1);
e.sinceRelease = 1e9f; // first batch releases immediately
e.emitAccum = 1.0f; // first particle immediately
e.issued = 0;
e.active = 1;
gBTPfxEmitters.push_back(e);
@@ -602,17 +602,21 @@ void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
BTPfxEmitter &e = gBTPfxEmitters[ei];
if (!e.active) continue;
const BTPfxDef &d = *e.def;
e.sinceRelease += dt;
if (e.sinceRelease >= d.releasePeriod)
// CONTINUOUS emission at `rate` particles/second until maximum_issue is
// exhausted. Data-verified semantics: in EVERY shipped .PFX,
// maxIssue/rate == release_period (DAFC 25/100~=0.2, DNBOOM 35/150~=0.2,
// DDAM2 35/16~=2, DDTHSMK 30/3=10) -- release_period IS the emission
// WINDOW, not a batch interval. (The old one-burst batching dumped a
// 10-second smoke plume in a single frame's puff.)
e.emitAccum += d.rate * dt;
int n = (int)e.emitAccum;
if (n > 0)
{
// one batch: rate particles/sec over the release period, total-capped
int batch = (int)(d.rate * (d.releasePeriod > 1e-4f ? d.releasePeriod : dt));
if (batch < 1) batch = 1;
if (e.issued + batch > d.maxIssue) batch = d.maxIssue - e.issued;
for (int b = 0; b < batch; ++b)
e.emitAccum -= (float)n;
if (e.issued + n > d.maxIssue) n = d.maxIssue - e.issued;
for (int b = 0; b < n; ++b)
BTPfxSpawn(e);
e.issued += batch;
e.sinceRelease = 0.0f;
e.issued += n;
if (e.issued >= d.maxIssue)
e.active = 0; // done issuing -> instance ends
}
+1
View File
@@ -1318,6 +1318,7 @@ Mech::Mech(
deathHandler = (int)new MechDeathHandler(this); // FUN_0042a984
critRes->Unlock();
}
wreckSmokeTimer = 0.0f;
//
// Cylinder hit-location table (resource type 0x1d = DamageLookupTableStream) --
+5 -1
View File
@@ -497,6 +497,10 @@ protected:
// "ammoExpended"; binary slot this[0x111] is this pointer.)
int damageLookupTable; // @0x444 this[0x111] (FUN_0049ea48)
int deathHandler; // @0x850 this[0x214] (FUN_0042a984)
// Wreck-smoke cadence (port addition, [T3]): re-fires the death/rubble
// smoke plume (psfx 1, DDTHSMK) every 10s (its authored emission window)
// while the mech is a destroyed wreck, so a dead mech keeps smoking.
Scalar wreckSmokeTimer;
// Three ref-counted creation-name objects (badge/color/insignia).
void *resourceNameA; // @0x844 this[0x211]
@@ -677,7 +681,7 @@ protected:
// --- death sequence (the un-exported master-perf death branch, rebuilt
// from its exported consumers + the RP VTV::DeathShutdown analog) ---
Logical IsMechDestroyed(); // damage-side death flag: graphicAlarm level >= 9
void UpdateDeathState(); // collapse movementMode + subsystem DeathShutdown + freeze
void UpdateDeathState(Scalar dt); // death freeze + subsystem DeathShutdown + wreck smoke
// --- clip loaders (mech3) -------------------------------------------
ResourceDescription::ResourceID *
+22 -2
View File
@@ -943,12 +943,32 @@ Logical
}
void
Mech::UpdateDeathState()
Mech::UpdateDeathState(Scalar dt)
{
if (!IsMechDestroyed()) // alive
return;
if (movementMode == 9) // already settled (disabled/frozen)
{
// WRECK SMOKE (port addition, [T3]): keep the dead hulk smoking -- re-fire
// the death/rubble smoke plume (psfx 1 = DDTHSMK, "the mech death/rubble
// smoke plume") each time its authored 10-second emission window elapses.
// The .PFX itself trickles 3 particles/sec for 10s; re-arming it on that
// same cadence reads as a continuously burning wreck. The one-shot death
// visuals (dnboom + skins) stay in the kill path.
wreckSmokeTimer -= dt;
if (wreckSmokeTimer <= 0.0f)
{
wreckSmokeTimer = 10.0f; // DDTHSMK's release window
extern void BTStartPfx(int effect_number, float x, float y, float z);
BTStartPfx(1,
(float)localOrigin.linearPosition.x,
(float)(localOrigin.linearPosition.y + kMuzzleHeight),
(float)localOrigin.linearPosition.z);
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[death] wreck smoke plume re-armed (psfx 1)\n" << std::flush;
}
return;
}
//
// DEATH = FREEZE, not collapse (decomp-verified 2026-07-08, task #32).
@@ -1001,7 +1021,7 @@ void
// vital kill it collapses + shuts the subsystems down + settles to disabled;
// the drive's movementMode write below is guarded on !IsMechDestroyed so a
// dead mech keeps its death state instead of reverting to a live gait.
UpdateDeathState();
UpdateDeathState(dt);
// MechDeathHandler (@0042aa2c): per-frame destroyed-skin + explosion effects as
// this mech's zones cross damage thresholds. The binary ticks it off the mech's