From b44c0f98f6c7d6355266e2243f57578f1b7da7e7 Mon Sep 17 00:00:00 2001 From: arcattack Date: Wed, 8 Jul 2026 15:13:10 -0500 Subject: [PATCH] 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 --- engine/MUNGA_L4/L4VIDEO.cpp | 26 +++++++++++++++----------- game/reconstructed/mech.cpp | 1 + game/reconstructed/mech.hpp | 6 +++++- game/reconstructed/mech4.cpp | 24 ++++++++++++++++++++++-- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 06b9eae..b06700b 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -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 } diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index b71bbd7..cd05c11 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -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) -- diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 3a7494c..f03079c 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -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 * diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 87b4be1..fcae855 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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