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:
co-authored by
Claude Fable 5
parent
7c455303bd
commit
a3d67cc639
+556
-5
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user