Combat visible + killable: Wword root-cause fix, .PFX effect layer, RemakeEntity swap

The 'can't kill the enemy / no visible damage' cluster, root-caused and fixed
faithfully:

- STEP-6 unaimed path was INERT: the cylinder table was 'cached' at Wword(0x111)
  -- the recon ABSORBER bank (stores nothing, reads 0) -- so every unaimed hit
  silently no-op'd.  Promoted to the named member Mech::damageLookupTable
  (binary this[0x111], was mislabeled ammoExpended).  New gotcha class recorded
  (reconstruction-gotchas §2) + sweep; 2 dead multiplayer branches logged.

- Fire path migrated off the stale vital-zone aim onto the completed STEP-6
  unaimed dispatch (zone=-1 + beam entry point -> cylinder resolves the
  exterior zone).  No more invisible 1-shot kills; death via the authentic
  cascade (~14 center-mass hits).  Wreck stays TARGETED on kill (beams stop on
  it); scoring latches off.

- SendSubsystemDamage AV fixed: unbound critical-subsystem plug guard (43
  unbound plugs/mech logged as an open question -- the binding itself is a gap).

- RemakeEntity (render damage swap): the 1996 render state machine's missing
  Remake state, reconstructed as an in-place SetDrawObj mesh swap keyed by each
  segment's damage-zone graphic state (tree dtor doesn't cascade -> never
  rebuild).  Destroyed arms/guns visibly wreck (the only variants the RES
  registers).

- BT .PFX particle layer (L4VIDEO.cpp): the 1995 explosion/damage effect layer,
  unported since 2007 (DPLIndependantEffect/ReadPSFX/ExplosionScripts all
  stubs).  Parses the authentic VIDEO/*.PFX definitions via the [pfx_day]
  psfxN mapping; premultiplied blending renders BOTH families from the same
  data (additive-style fire + occluding smoke -- DDAM2 is 30% grey, DDTHSMK
  ramps negative: impossible additively); depth-sorted billboards with a
  radial-masked grit sprite; impact-frame orientation (.PFX offsets are
  authored mech-local, -Z = out of the struck armor toward the shooter) for
  weapon hits AND damage bands (via lastInflictingID, now maintained -- was
  declared but never written).  Both effect-number encodings route (raw dpl
  <100 + WinTesla 1000+slot carried by the band resources).  Death fires the
  authentic dnboom (7) + ddthsmk smoke plume (1).

- Effects anchor at the impact point / damaged zone's segment, not the mech
  origin (no more fire at the feet).

- Dev force-input gates BT_AUTOFIRE / BT_AUTODRIVE for headless fire-chain
  verification; BT_PFX_ADD=1 flips the particle blend for A/B.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 14:43:32 -05:00
co-authored by Claude Fable 5
parent 7c455303bd
commit a3d67cc639
16 changed files with 1205 additions and 62 deletions
+556 -5
View File
@@ -25,6 +25,7 @@ using namespace std;
#include <vector>
#include <hash_map>
#include <map>
#include <algorithm> // std::sort (the .PFX particle depth sort)
LPDIRECT3D9 gD3D = NULL;
@@ -58,6 +59,11 @@ struct BTBeamFx
};
static std::vector<BTBeamFx> gBTBeams;
// the decoded beam grit sheet (built lazily in BTDrawBeams; shared with the
// .PFX particle layer below)
LPDIRECT3DTEXTURE9 gBTBeamGritTex = 0;
LPDIRECT3DTEXTURE9 BTGetBeamGritTexture() { return gBTBeamGritTex; }
// Called from the game (mech4.cpp) -- external linkage, matched by an extern decl there.
// LOD eyepoint feed (game -> renderer): the viewpoint mech's position, the
// authentic LOD reference (see the ExecuteImplementation banner). mech4 calls
@@ -121,8 +127,10 @@ void BTDrawBeams(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
// once, ramp its luminance to a grayscale grit texture, and MODULATE the beam
// colour by it with a scrolling UV -> the streaming "gritty" look, not a clean
// gradient. BT_BEAM_TEX=0 falls back to the plain additive beam.
// (file-scope so the .PFX particle layer below can share the decoded sheet)
static int s_beamTexTried = 0;
static LPDIRECT3DTEXTURE9 s_beamTex = 0;
extern LPDIRECT3DTEXTURE9 gBTBeamGritTex;
LPDIRECT3DTEXTURE9 &s_beamTex = gBTBeamGritTex;
if (!s_beamTexTried)
{
s_beamTexTried = 1;
@@ -361,6 +369,484 @@ void BTDrawBeams(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
dev->SetSamplerState(0, D3DSAMP_ADDRESSV, sAddrV);
}
//===========================================================================//
// BT PARTICLE EFFECTS (.PFX) -- the 1995 explosion/damage effect layer.
//
// The pod's damage/explosion visuals are DATA: VIDEO/*.PFX text files (format
// documented inside each file), mapped to dpl effect NUMBERS by BTDPL.INI's
// [pfx_day]/[pfx_night] pages ("psfxN=file.pfx" -- N is the number embedded in
// resources: 2..5 = the mech damage bands, 6 = projectile-gun hit, 7 = the
// mech-death explosion, 8 = zone destroyed...). The 1995 board consumed these
// through dpl particle calls that were never ported: DPLIndependantEffect's
// (<100) arm and ReadPSFX are both stubbed, so every weapon-hit / damage-band /
// death explosion rendered NOTHING. Like the weapon-beam layer above, this is
// the self-contained D3D9 port of that layer:
// BTLoadPfxFile -- parses a .PFX into BTPfxDef (the documented format)
// BTStartPfx -- starts an emitter instance at a world position
// (called by DPLIndependantEffect for effect_number < 100)
// BTDrawPfx -- per-frame sim + camera-facing additive-quad billboards
// (called beside BTDrawBeams in the render loop)
// Variance convention (decoded from DNBOOM.PFX: vel 150 + var -300 = a
// symmetric +-150 burst): sampled = value + variance * rand01().
// NOT yet honoured (noted, low-visibility): atten/attenv (distance
// attenuation), colorWarp/alphaWarp exponents are applied as t^(1/warp),
// the per-file texture name (all BT effects use the firesmoke sheet; we use
// the same decoded grit texture as the beams).
//===========================================================================//
struct BTPfxDef
{
int valid;
unsigned identifier;
int maxIssue; // total particles an instance may issue
float releasePeriod; // batch interval (s)
float rate; // particles per second while releasing
float px, py, pz, pv; // spawn offset + positional variance
float velx, vely, velz, velxv, velyv, velzv;
float rad, radv, exp, expv, dexp, dexpv; // radius, expansion rate, expansion decay
float accelx, accely, accelz, accelxv, accelyv, accelzv;
float atten, attenv; // (not yet honoured)
float sI[4], sIv[4]; // start colour inner RGBA + variance
float sO[4], sOv[4]; // start colour outer RGBA + variance
float eI[4], eIv[4]; // end colour inner RGBA + variance
float eO[4], eOv[4]; // end colour outer RGBA + variance
float colorWarp, alphaWarp;
float duration, durationv; // particle lifetime
};
#define BT_PFX_SLOTS 32
static BTPfxDef gBTPfxDefs[BT_PFX_SLOTS];
struct BTPfxEmitter
{
const BTPfxDef *def;
D3DXVECTOR3 pos;
// The effect's LOCAL FRAME (.PFX offsets/velocities are authored in the
// 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;
int issued;
int active;
};
struct BTPfxParticle
{
D3DXVECTOR3 pos, vel, accel;
float age, life;
float rad, exp, dexp;
float colorWarp, alphaWarp; // def-level warps, carried per particle
float sI[4], sO[4], eI[4], eO[4];
};
static std::vector<BTPfxEmitter> gBTPfxEmitters;
static std::vector<BTPfxParticle> gBTPfxParticles;
static float BTPfxRand01() // cheap deterministic LCG
{
static unsigned s = 0x2545F491u;
s = s * 1664525u + 1013904223u;
return (float)((s >> 8) & 0xFFFFFF) / 16777215.0f;
}
// Parse one .PFX text file (VIDEO\<name>) into a def slot. Format per the
// spec block carried inside every .PFX (and ReadPSFX's comment).
static int BTLoadPfxFile(const char *file_name, BTPfxDef &d)
{
char path[256];
strcpy(path, "VIDEO\\");
strcat(path, file_name);
FILE *fp = fopen(path, "rt");
if (!fp)
{
DEBUG_STREAM << "[pfx] could not open " << path << " -- its effects will not draw\n" << std::flush;
return 0;
}
memset(&d, 0, sizeof(d));
char line[256];
int ok = 1;
// line 1: texture name (recorded in the banner note; the shared sheet is used)
if (!fgets(line, sizeof(line), fp)) ok = 0;
if (ok && fgets(line, sizeof(line), fp))
ok = (sscanf(line, "%x %d %f %f", &d.identifier, &d.maxIssue, &d.releasePeriod, &d.rate) == 4);
else ok = 0;
if (ok && fgets(line, sizeof(line), fp))
ok = (sscanf(line, "%f %f %f %f", &d.px, &d.py, &d.pz, &d.pv) == 4);
else ok = 0;
if (ok && fgets(line, sizeof(line), fp))
ok = (sscanf(line, "%f %f %f %f %f %f", &d.velx, &d.vely, &d.velz, &d.velxv, &d.velyv, &d.velzv) == 6);
else ok = 0;
if (ok && fgets(line, sizeof(line), fp))
ok = (sscanf(line, "%f %f %f %f %f %f", &d.rad, &d.radv, &d.exp, &d.expv, &d.dexp, &d.dexpv) == 6);
else ok = 0;
if (ok && fgets(line, sizeof(line), fp))
ok = (sscanf(line, "%f %f %f %f %f %f", &d.accelx, &d.accely, &d.accelz, &d.accelxv, &d.accelyv, &d.accelzv) == 6);
else ok = 0;
if (ok && fgets(line, sizeof(line), fp))
ok = (sscanf(line, "%f %f", &d.atten, &d.attenv) == 2);
else ok = 0;
float *quads[8] = { d.sI, d.sIv, d.sO, d.sOv, d.eI, d.eIv, d.eO, d.eOv };
for (int q = 0; ok && q < 4; ++q) // 4 lines: sI+var, sO+var, eI+var, eO+var
{
if (fgets(line, sizeof(line), fp))
ok = (sscanf(line, "%f %f %f %f %f %f %f %f",
&quads[q*2][0], &quads[q*2][1], &quads[q*2][2], &quads[q*2][3],
&quads[q*2+1][0], &quads[q*2+1][1], &quads[q*2+1][2], &quads[q*2+1][3]) == 8);
else ok = 0;
}
if (ok && fgets(line, sizeof(line), fp))
ok = (sscanf(line, "%f %f", &d.colorWarp, &d.alphaWarp) == 2);
else ok = 0;
if (ok && fgets(line, sizeof(line), fp))
ok = (sscanf(line, "%f %f", &d.duration, &d.durationv) == 2);
else ok = 0;
fclose(fp);
if (!ok)
{
DEBUG_STREAM << "[pfx] " << path << " did not parse -- effect disabled\n" << std::flush;
return 0;
}
d.valid = 1;
return 1;
}
// Slot-checked load entry used by the psfx page loader (LoadMission walk).
int BTLoadPfxFile_slot(const char *file_name, int slot)
{
if (slot < 0 || slot >= BT_PFX_SLOTS)
return 0;
return BTLoadPfxFile(file_name, gBTPfxDefs[slot]);
}
// Start one effect instance at a world position (the DPLIndependantEffect
// contract: renderer-owned, runs to termination on its own). The optional
// frame rows orient the .PFX's mech-local offsets/velocities in the world
// (pass the victim entity's localToWorld X/Y/Z rows); identity when absent.
void BTStartPfxFrame(int effect_number, float x, float y, float z,
const float *xrow, const float *yrow, const float *zrow)
{
if (effect_number < 0 || effect_number >= BT_PFX_SLOTS)
return;
const BTPfxDef &d = gBTPfxDefs[effect_number];
if (!d.valid)
return;
if (gBTPfxEmitters.size() > 64) // runaway guard
return;
BTPfxEmitter e;
e.def = &d;
e.pos = D3DXVECTOR3(x, y, 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.issued = 0;
e.active = 1;
gBTPfxEmitters.push_back(e);
}
void BTStartPfx(int effect_number, float x, float y, float z)
{
BTStartPfxFrame(effect_number, x, y, z, 0, 0, 0);
}
static void BTPfxSpawn(const BTPfxEmitter &e)
{
const BTPfxDef &d = *e.def;
if (gBTPfxParticles.size() > 2048) // global particle cap
return;
BTPfxParticle p;
// Sample in the effect's LOCAL frame, then orient into the world through
// the emitter's basis (the victim's localToWorld rows). The position
// jitter pv is an isotropic scatter about the base offset -> symmetric
// (rand +-pv); the paired variances stay value + var*rand01 (DNBOOM's
// vel 150 / var -300 decodes to the symmetric +-150 burst).
D3DXVECTOR3 lp(
d.px + d.pv * (BTPfxRand01() * 2.0f - 1.0f),
d.py + d.pv * (BTPfxRand01() * 2.0f - 1.0f),
d.pz + d.pv * (BTPfxRand01() * 2.0f - 1.0f));
D3DXVECTOR3 lv(
d.velx + d.velxv * BTPfxRand01(),
d.vely + d.velyv * BTPfxRand01(),
d.velz + d.velzv * BTPfxRand01());
D3DXVECTOR3 la(
d.accelx + d.accelxv * BTPfxRand01(),
d.accely + d.accelyv * BTPfxRand01(),
d.accelz + d.accelzv * BTPfxRand01());
p.pos = e.pos + e.ax * lp.x + e.ay * lp.y + e.az * lp.z;
p.vel = e.ax * lv.x + e.ay * lv.y + e.az * lv.z;
p.accel = e.ax * la.x + e.ay * la.y + e.az * la.z;
p.age = 0.0f;
p.life = d.duration + d.durationv * BTPfxRand01();
if (p.life < 0.05f) p.life = 0.05f;
p.rad = d.rad + d.radv * BTPfxRand01();
p.exp = d.exp + d.expv * BTPfxRand01();
p.dexp = d.dexp + d.dexpv * BTPfxRand01();
p.colorWarp = (d.colorWarp > 1e-3f) ? d.colorWarp : 1.0f;
p.alphaWarp = (d.alphaWarp > 1e-3f) ? d.alphaWarp : 1.0f;
for (int i = 0; i < 4; ++i)
{
p.sI[i] = d.sI[i] + d.sIv[i] * BTPfxRand01();
p.sO[i] = d.sO[i] + d.sOv[i] * BTPfxRand01();
p.eI[i] = d.eI[i] + d.eIv[i] * BTPfxRand01();
p.eO[i] = d.eO[i] + d.eOv[i] * BTPfxRand01();
}
gBTPfxParticles.push_back(p);
}
// The beams' decoded grit texture (built in BTDrawBeams) -- shared with the
// particles so the fire quads carry the authentic firesmoke-family noise.
extern LPDIRECT3DTEXTURE9 BTGetBeamGritTexture();
void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt)
{
// ---- sim ----
if (dt > 0.1f) dt = 0.1f; // stall guard
for (size_t ei = 0; ei < gBTPfxEmitters.size(); ++ei)
{
BTPfxEmitter &e = gBTPfxEmitters[ei];
if (!e.active) continue;
const BTPfxDef &d = *e.def;
e.sinceRelease += dt;
if (e.sinceRelease >= d.releasePeriod)
{
// 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)
BTPfxSpawn(e);
e.issued += batch;
e.sinceRelease = 0.0f;
if (e.issued >= d.maxIssue)
e.active = 0; // done issuing -> instance ends
}
}
{ // compact finished emitters
size_t w = 0;
for (size_t i = 0; i < gBTPfxEmitters.size(); ++i)
if (gBTPfxEmitters[i].active) gBTPfxEmitters[w++] = gBTPfxEmitters[i];
gBTPfxEmitters.resize(w);
}
{ // advance + expire particles
size_t w = 0;
for (size_t i = 0; i < gBTPfxParticles.size(); ++i)
{
BTPfxParticle &p = gBTPfxParticles[i];
p.age += dt;
if (p.age >= p.life) continue;
p.vel += p.accel * dt;
p.pos += p.vel * dt;
p.rad += p.exp * dt;
p.exp += p.dexp * dt;
if (p.rad < 0.05f) p.rad = 0.05f;
gBTPfxParticles[w++] = p;
}
gBTPfxParticles.resize(w);
}
if (gBTPfxParticles.empty())
return;
// ---- draw: camera-facing additive quads (inner core + outer glow) ----
const D3DXVECTOR3 right(view->_11, view->_21, view->_31);
const D3DXVECTOR3 up (view->_12, view->_22, view->_32);
DWORD sLight, sFog, sZW, sBlend, sSrc, sDst, sCull, sCop, sCa1, sCa2, sAop, sAa1, sAa2;
dev->GetRenderState(D3DRS_LIGHTING, &sLight);
dev->GetRenderState(D3DRS_FOGENABLE, &sFog);
dev->GetRenderState(D3DRS_ZWRITEENABLE, &sZW);
dev->GetRenderState(D3DRS_ALPHABLENDENABLE, &sBlend);
dev->GetRenderState(D3DRS_SRCBLEND, &sSrc);
dev->GetRenderState(D3DRS_DESTBLEND, &sDst);
dev->GetRenderState(D3DRS_CULLMODE, &sCull);
dev->GetTextureStageState(0, D3DTSS_COLOROP, &sCop);
dev->GetTextureStageState(0, D3DTSS_COLORARG1, &sCa1);
dev->GetTextureStageState(0, D3DTSS_COLORARG2, &sCa2);
dev->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAop);
dev->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAa1);
dev->GetTextureStageState(0, D3DTSS_ALPHAARG2, &sAa2);
// PREMULTIPLIED blending (ONE, INVSRCALPHA): the one model that renders BOTH
// authored particle families correctly. FIRE (bright colour, the additive
// glow) and SMOKE (dark/negative-ramp colour with high alpha -> OCCLUDES the
// scene behind it -- DDAM2 is 30% grey, DDTHSMK ramps to negative: both are
// invisible under pure additive, which is why damaged mechs never smoked).
// Vertex colour carries colour*alpha (premultiplied), vertex alpha carries
// the occlusion; the texture's falloff mask modulates both.
// BT_PFX_ADD=1 flips back to pure additive for A/B comparison.
static int s_pfxAdditive = -1;
if (s_pfxAdditive < 0)
{
const char *av = getenv("BT_PFX_ADD");
s_pfxAdditive = (av != 0 && av[0] == '1') ? 1 : 0;
}
dev->SetRenderState(D3DRS_LIGHTING, FALSE);
dev->SetRenderState(D3DRS_FOGENABLE, FALSE);
dev->SetRenderState(D3DRS_ZWRITEENABLE, FALSE); // test Z, don't write it
dev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
dev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
dev->SetRenderState(D3DRS_DESTBLEND, s_pfxAdditive ? D3DBLEND_ONE : D3DBLEND_INVSRCALPHA);
dev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
// Particle sprite texture: the beams' authentic grit sheet MASKED by a
// radial falloff -> soft round fiery blobs (an unmasked square sheet on an
// additive quad reads as a hard BOX, which the 1995 sprites never did).
// Baked once from the decoded grit; pure radial gradient if grit is absent.
static LPDIRECT3DTEXTURE9 s_pfxTex = 0;
static int s_pfxTexTried = 0;
if (!s_pfxTexTried)
{
s_pfxTexTried = 1;
const int TW = 64, TH = 64;
LPDIRECT3DTEXTURE9 grit = BTGetBeamGritTexture();
unsigned char gritLum[128 * 64]; // grit is 128x64 when present
int gw = 0, gh = 0;
if (grit)
{
D3DSURFACE_DESC gd;
if (SUCCEEDED(grit->GetLevelDesc(0, &gd)) && gd.Width <= 128 && gd.Height <= 64)
{
D3DLOCKED_RECT glr;
if (SUCCEEDED(grit->LockRect(0, &glr, NULL, D3DLOCK_READONLY)))
{
gw = gd.Width; gh = gd.Height;
for (int y = 0; y < gh; ++y)
{
const DWORD *src = (const DWORD *)((const char *)glr.pBits + y * glr.Pitch);
for (int x = 0; x < gw; ++x)
gritLum[y * gw + x] = (unsigned char)(src[x] & 0xFF);
}
grit->UnlockRect(0);
}
}
}
if (SUCCEEDED(dev->CreateTexture(TW, TH, 1, 0, D3DFMT_A8R8G8B8,
D3DPOOL_MANAGED, &s_pfxTex, NULL)))
{
D3DLOCKED_RECT lr;
if (SUCCEEDED(s_pfxTex->LockRect(0, &lr, NULL, 0)))
{
for (int y = 0; y < TH; ++y)
{
DWORD *dst = (DWORD *)((char *)lr.pBits + y * lr.Pitch);
for (int x = 0; x < TW; ++x)
{
float dx = (x + 0.5f) / TW * 2.0f - 1.0f;
float dy = (y + 0.5f) / TH * 2.0f - 1.0f;
float r = sqrtf(dx * dx + dy * dy);
float f = 1.0f - r; // radial falloff
if (f < 0.0f) f = 0.0f;
f = f * f * (3.0f - 2.0f * f); // smoothstep edge
float n = 1.0f;
if (gw > 0) // authentic grit detail
n = 0.35f + 0.65f * (gritLum[(y % gh) * gw + (x % gw)] / 255.0f);
int v = (int)(f * n * 255.0f);
if (v > 255) v = 255;
// the falloff mask lives in ALPHA too: the premultiplied
// draw (ONE, INVSRCALPHA) needs soft-edged OCCLUSION for
// smoke, not just soft-edged colour for fire
dst[x] = (v << 24) | (v << 16) | (v << 8) | v;
}
}
s_pfxTex->UnlockRect(0);
}
else { s_pfxTex->Release(); s_pfxTex = 0; }
}
}
dev->SetTexture(0, s_pfxTex);
dev->SetTextureStageState(0, D3DTSS_COLOROP, s_pfxTex ? D3DTOP_MODULATE : D3DTOP_SELECTARG1);
dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_DIFFUSE);
dev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TEXTURE);
// occlusion = vertex alpha x the texture's falloff mask (soft-edged smoke)
dev->SetTextureStageState(0, D3DTSS_ALPHAOP, s_pfxTex ? D3DTOP_MODULATE : D3DTOP_SELECTARG1);
dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);
dev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TEXTURE);
D3DXMATRIX ident; D3DXMatrixIdentity(&ident);
dev->SetTransform(D3DTS_WORLD, &ident);
dev->SetFVF(D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1);
struct V { float x, y, z; DWORD c; float u, v; };
static std::vector<V> verts;
verts.clear();
// BACK-TO-FRONT order: premultiplied occlusion (smoke) composites in depth
// order; unsorted draws pop when a near puff renders before a far one.
static std::vector<size_t> order;
static std::vector<float> depth;
order.resize(gBTPfxParticles.size());
depth.resize(gBTPfxParticles.size());
for (size_t i = 0; i < gBTPfxParticles.size(); ++i)
{
const D3DXVECTOR3 &pp = gBTPfxParticles[i].pos;
order[i] = i;
depth[i] = pp.x * view->_13 + pp.y * view->_23 + pp.z * view->_33; // view-space z
}
std::sort(order.begin(), order.end(),
[](size_t a, size_t b) { return depth[a] > depth[b]; });
for (size_t oi = 0; oi < order.size(); ++oi)
{
const BTPfxParticle &p = gBTPfxParticles[order[oi]];
float t = p.age / p.life;
if (t < 0.0f) t = 0.0f; if (t > 1.0f) t = 1.0f;
// colour/alpha interpolation start->end, warped (t^(1/warp): a large
// warp shifts to the end colour early -- the fast orange->smoke shift)
float tc = powf(t, 1.0f / p.colorWarp);
float ta = powf(t, 1.0f / p.alphaWarp);
// two quads: outer glow (2.2x radius) then inner core
for (int layer = 0; layer < 2; ++layer)
{
const float *cs = layer ? p.sI : p.sO;
const float *ce = layer ? p.eI : p.eO;
float scale = layer ? 1.0f : 2.2f;
float r = cs[0] + (ce[0] - cs[0]) * tc;
float g = cs[1] + (ce[1] - cs[1]) * tc;
float b = cs[2] + (ce[2] - cs[2]) * tc;
float a = cs[3] + (ce[3] - cs[3]) * ta;
if (a <= 0.0f) continue;
if (a > 1.0f) a = 1.0f;
// PREMULTIPLIED: vertex rgb = colour x alpha (the framebuffer
// contribution), vertex a = alpha (the occlusion). A colour ramped
// NEGATIVE (DDTHSMK's smoke tail) clamps to 0 -> pure occluding
// black smoke; a bright fire colour with fading alpha dims out.
int ir = (int)(r * a * 255.0f); if (ir > 255) ir = 255; if (ir < 0) ir = 0;
int ig = (int)(g * a * 255.0f); if (ig > 255) ig = 255; if (ig < 0) ig = 0;
int ib = (int)(b * a * 255.0f); if (ib > 255) ib = 255; if (ib < 0) ib = 0;
int ia = (int)(a * 255.0f); if (ia > 255) ia = 255; if (ia < 0) ia = 0;
if (ia == 0 && ir == 0 && ig == 0 && ib == 0) continue;
DWORD c = ((DWORD)ia << 24) | (ir << 16) | (ig << 8) | ib;
float rad = p.rad * scale;
D3DXVECTOR3 rv = right * rad, uv = up * rad;
V q[6];
q[0].x = p.pos.x - rv.x - uv.x; q[0].y = p.pos.y - rv.y - uv.y; q[0].z = p.pos.z - rv.z - uv.z; q[0].u = 0; q[0].v = 1;
q[1].x = p.pos.x - rv.x + uv.x; q[1].y = p.pos.y - rv.y + uv.y; q[1].z = p.pos.z - rv.z + uv.z; q[1].u = 0; q[1].v = 0;
q[2].x = p.pos.x + rv.x + uv.x; q[2].y = p.pos.y + rv.y + uv.y; q[2].z = p.pos.z + rv.z + uv.z; q[2].u = 1; q[2].v = 0;
q[3] = q[0];
q[4] = q[2];
q[5].x = p.pos.x + rv.x - uv.x; q[5].y = p.pos.y + rv.y - uv.y; q[5].z = p.pos.z + rv.z - uv.z; q[5].u = 1; q[5].v = 1;
for (int k = 0; k < 6; ++k) { q[k].c = c; verts.push_back(q[k]); }
}
}
if (!verts.empty())
dev->DrawPrimitiveUP(D3DPT_TRIANGLELIST, (UINT)verts.size() / 3,
&verts[0], sizeof(V));
dev->SetTexture(0, NULL);
dev->SetRenderState(D3DRS_LIGHTING, sLight);
dev->SetRenderState(D3DRS_FOGENABLE, sFog);
dev->SetRenderState(D3DRS_ZWRITEENABLE, sZW);
dev->SetRenderState(D3DRS_ALPHABLENDENABLE, sBlend);
dev->SetRenderState(D3DRS_SRCBLEND, sSrc);
dev->SetRenderState(D3DRS_DESTBLEND, sDst);
dev->SetRenderState(D3DRS_CULLMODE, sCull);
dev->SetTextureStageState(0, D3DTSS_COLOROP, sCop);
dev->SetTextureStageState(0, D3DTSS_COLORARG1, sCa1);
dev->SetTextureStageState(0, D3DTSS_COLORARG2, sCa2);
dev->SetTextureStageState(0, D3DTSS_ALPHAOP, sAop);
dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAa1);
dev->SetTextureStageState(0, D3DTSS_ALPHAARG2, sAa2);
}
// BT (task #20): the live window client aspect (0 = never resized -> the
// projection builders fall back to the configured x_size/y_size). Set by
// L4NotifyWindowResized (WM_SIZE) so a user-resized window doesn't stretch
@@ -3575,8 +4061,32 @@ void
Unregister_Object(light_namelist);
delete light_namelist;
}
//
// Get the list of PSFX effects we should load into RAM. RECONSTRUCTED
// (was stubbed with ReadPSFX): the "psfxN=file.pfx" entries on the visited
// pages ([pfx_day]/[pfx_night], include-reached from the mission's arena
// page) bind each dpl effect NUMBER to its authentic .PFX definition,
// loaded into the BT particle layer (BTLoadPfxFile / BTStartPfx / BTDrawPfx
// at the top of this file) that DPLIndependantEffect's <100 arm consumes.
//
if ((psfx_namelist = master_notation_file->MakeEntryList(starting_page_name, "psfx")) != NULL)
{
extern int BTLoadPfxFile_slot(const char *file_name, int slot);
int pfx_loaded = 0;
for (entry = psfx_namelist->GetFirstEntry();
entry;
entry = entry->GetNextEntry())
{
int psfx_number = atoi(entry->GetName() + 4); // "psfxN"
const char *psfx_file_name = entry->GetChar();
pfx_loaded += BTLoadPfxFile_slot(psfx_file_name, psfx_number);
}
DEBUG_STREAM << "[pfx] page '" << starting_page_name << "': "
<< pfx_loaded << " effect definitions loaded\n" << std::flush;
delete psfx_namelist;
}
////
//// Get a the list of PSFX effects we should load into RAM
//// (original stubbed loader retained for provenance)
////
//if ((psfx_namelist = master_notation_file->MakeEntryList(starting_page_name, "psfx")) != NULL)
//{
@@ -5894,9 +6404,29 @@ void
<< entity->localOrigin.linearPosition.x
<< ", " << entity->localOrigin.linearPosition.y
<< ", " << entity->localOrigin.linearPosition.z << "], " << effect_number << ");" << std::endl << std::flush;
DPLIndependantEffect(
entity->localOrigin.linearPosition,
effect_number);
// Both effect-number ENCODINGS resolve to the same psfx slot:
// <100 = the raw dpl board number; >=1000 = the WinTesla-era
// "1000+slot" INDIE id carried by the damage-band resources
// (1002-1005 = ddam1-4 light..critical damage smoke, 1008 =
// ddam5 zone-destroyed -- the SAME [pfx_day] mapping). BT
// ships no INDIE descriptors (the version-2 specialfx pages
// don't exist), so all slots route to the BT .PFX layer, with
// the Explosion entity's ORIENTATION (the victim's frame) --
// the .PFX offsets/velocities are authored mech-local.
{
int pfx_slot = (effect_number >= 1000)
? effect_number - 1000 : effect_number;
extern void BTStartPfxFrame(int, float, float, float,
const float *, const float *, const float *);
float xr[3] = { (float)entity->localToWorld(0,0), (float)entity->localToWorld(0,1), (float)entity->localToWorld(0,2) };
float yr[3] = { (float)entity->localToWorld(1,0), (float)entity->localToWorld(1,1), (float)entity->localToWorld(1,2) };
float zr[3] = { (float)entity->localToWorld(2,0), (float)entity->localToWorld(2,1), (float)entity->localToWorld(2,2) };
BTStartPfxFrame(pfx_slot,
(float)entity->localOrigin.linearPosition.x,
(float)entity->localOrigin.linearPosition.y,
(float)entity->localOrigin.linearPosition.z,
xr, yr, zr);
}
}
else
{
@@ -6928,6 +7458,13 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
// beam is occluded where it enters terrain). See BTDrawBeams above.
BTDrawBeams(mDevice, &viewTransform, (float)dT);
// BT .PFX particle effects (explosions / damage bands / mech death) -- the
// same pass as the beams: world proj+view set, Z-test on, additive quads.
{
extern void BTDrawPfx(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt);
BTDrawPfx(mDevice, &viewTransform, (float)dT);
}
//
// And don't forget particles too
//
@@ -7565,6 +8102,20 @@ void
return;
}
//
// Board effect numbers (<100) -- the 1995 dpl explosion/damage effects.
// RECONSTRUCTED: routed to the BT .PFX particle layer (the [pfx_day]/
// [pfx_night] psfxN mapping loads each number's authentic .PFX definition;
// see the layer banner at the top of this file). This is what makes every
// weapon-hit / damage-band / mech-death explosion actually VISIBLE -- the
// original dpl_Effect(dpl_effect_type_explosion, ...) below was IG-board
// hardware and was never ported.
//
{
extern void BTStartPfx(int effect_number, float x, float y, float z);
BTStartPfx(effect_number, location.x, location.y, location.z);
}
//dpl_EXPLOSION_EFFECT_INFO my_explosion;
//my_explosion.type = effect_number;
//my_explosion.x = location.x;
+5
View File
@@ -70,6 +70,11 @@ public:
virtual bool IsStatic() { return false; }
d3d_OBJECT *GetDrawObj() { return this->graphicalObject; }
// Swap this component's drawable in place. Execute() re-reads graphicalObject
// every frame, so changing it makes the segment draw a different mesh next
// frame -- the mechanism the BT damage-model "RemakeEntity" swap uses to show
// a destroyed segment without tearing down and rebuilding the whole tree.
void SetDrawObj(d3d_OBJECT *obj) { this->graphicalObject = obj; }
protected:
void ExecuteChildren();