diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index f06593f..025f4b0 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -7788,9 +7788,10 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte BTDrawPfx(mDevice, &viewTransform, (float)dT); } - // BT translocation spheres (task #52): the "blue warp" that collapses onto a - // dying mech and expands to reveal the reborn one -- one per watched player, - // keyed on its SimulationState dial. Same alpha pass as the beams/pfx. + // BT translocation spheres (task #52): the authentic POVTranslocateRenderable + // warp -- collapse onto the dying eye, throb black, then expand-reveal the reborn + // world. Drawn out-of-band here (as SKY, opaque) even while IsDead so the sphere + // survives the world mask it itself raises via BTSetWorldDead (below). { extern void BTDrawTranslocationSpheres(LPDIRECT3DDEVICE9 dev, const D3DXMATRIX *view, float dt, Time frame_time); diff --git a/engine/MUNGA_L4/L4VIDRND.cpp b/engine/MUNGA_L4/L4VIDRND.cpp index 7b8a687..2001e2b 100644 --- a/engine/MUNGA_L4/L4VIDRND.cpp +++ b/engine/MUNGA_L4/L4VIDRND.cpp @@ -1746,6 +1746,22 @@ void //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Constructor for POVTranslocateRenderable // +// BT (task #52): world-mask bridge for the reconstructed translocation warp. The +// authentic POVTranslocateRenderable toggles l4_application->SetIsDead around its +// collapse/expand (L4VIDRND.cpp:1947/1989) to black out the world during the death +// throb and reveal it on rebirth. Our reconstruction drives the warp from +// btl4vid.cpp (no L4Application handle there), so expose the same flag through this +// bridge. It is a PURE render-pass gate (verified: every IsDead() reader is a draw +// loop -- terrain/decal/sky/alpha/particles/reticle; no camera/input/targeting), so +// driving it directly is safe -- unlike pulsing the SimulationState trigger, which +// also drives the camera/POV + targeting (the f053535 regression). +void BTSetWorldDead(int dead) +{ + L4Application *l4_application = Cast_Object(L4Application*, application); + if (l4_application != 0) + l4_application->SetIsDead(dead != 0); +} + POVTranslocateRenderable::POVTranslocateRenderable( Entity *entity, // Entity to attach the renderable to ExecutionType execution_type, // How/when to execute the renderable diff --git a/game/reconstructed/btl4vid.cpp b/game/reconstructed/btl4vid.cpp index e67c14d..0db4b8c 100644 --- a/game/reconstructed/btl4vid.cpp +++ b/game/reconstructed/btl4vid.cpp @@ -2207,26 +2207,32 @@ Logical // tree still builds for the replicant/POV wiring are now inert.) //===========================================================================// namespace { - // Timings + scales are the engine's #defines verbatim (L4VIDRND.cpp:1763-1766): - // collapse from 100x down to 1x over 1.3s, then expand 1x -> 150x over 1.0s (the - // "blast off into the distance"). The big scales are CORRECT because the sphere - // is centred on your own eye (POV) -- you are meant to be deep inside it as it - // collapses through you and blasts back open. Tunable via BT_WARP_SCALE. - const float TLOC_COLLAPSE_TIME = 1.3f; - const float TLOC_EXPAND_TIME = 1.0f; + // The engine #defines VERBATIM (L4VIDRND.cpp:1763-1770): collapse from 100x down + // to 1x over 1.3s, throb at 1x, then expand 1x -> 150x over 1.0s. TRANSLATE_LIMIT + // is the WaitForReincarnate wobble amplitude. Only these five are live (the + // ROTATE_* / TRANSLATE_RATE members are dead -- no geometry rotation). + const float TLOC_COLLAPSE_TIME = 1.3f; // COLLAPSE_TIME + const float TLOC_EXPAND_TIME = 1.0f; // EXPAND_TIME + const float TLOC_TRANSLATE_LIMIT = 2.0f; // wait wobble amplitude float gWarpCollapseScale = 100.0f; // COLLAPSE_START_SCALE float gWarpExpandScale = 150.0f; // EXPAND_END_SCALE d3d_OBJECT *gTLocSphere = 0; int gTLocSphereTried = 0; - // The single active warp one-shot (one local player per node). - int gWarpPhase = 0; // 0 idle, 1 collapse, 2 expand - float gWarpT = 0.0f; + // The single active warp one-shot (one local player per node). Faithful state + // machine: 0 Idle, 1 InitialCollapse, 3 WaitForReincarnate (world masked, wobble), + // 2 ExpandReveal (L4VIDRND.cpp POVTranslocateRenderable states). + int gWarpPhase = 0; + float gWarpT = 0.0f; // phase clock (collapse/expand) + float gWaitClock = 0.0f; // WaitForReincarnate elapsed (wobble + stuck-black failsafe) float gWarpX = 0.0f, gWarpY = 0.0f, gWarpZ = 0.0f; - int gWarpPOV = 0; // 1 = centre on the local eye (own respawn); 0 = world-anchored (peer) + int gWarpPOV = 0; // 1 = centre on the local eye (own death/respawn); 0 = world-anchored (peer) + int gWarpMasked = 0; // 1 while we have raised the SetIsDead world mask } +extern void BTSetWorldDead(int dead); // L4VIDRND.cpp bridge -> l4_application->SetIsDead + // The renderable objects the entity tree builds for the translocation wiring are // inert now (the warp is the self-contained one-shot below); the ctor/dtor just // satisfy MakeEntityRenderables. @@ -2249,45 +2255,76 @@ BTTranslocationRenderable::~BTTranslocationRenderable() { } -// -// Kick the warp one-shot. Does NOT touch player state -- pure render. -// pov=1 -> centre it on the LOCAL eye (your own respawn): the authentic -// POV-tunnel that wraps your view. -// pov=0 -> anchor it at world (x,y,z): an OBSERVER seeing a peer respawn. -// -static void - BTKickWarp(int pov, float x, float y, float z) +static void BTWarpApplyScaleEnv() { if (const char *s = getenv("BT_WARP_SCALE")) { float v = (float)atof(s); if (v > 0.0f) { gWarpCollapseScale = v; gWarpExpandScale = v * 1.5f; } } - // RESPAWN = birth = the EXPAND phase (scale 1 -> 150 over 1s): the swirl bursts - // from the reinsertion point and blasts outward past you ("shoots off into the - // distance"). The COLLAPSE (100 -> 1) is the DEATH warp -- a separate trigger in - // the authentic engine (POVTranslocateRenderable), not part of the respawn -- so - // starting a respawn with a collapse read as "fades in then disappears". - // BT_WARP_COLLAPSE=1 plays the full collapse->expand cycle. - { - static int s_collapse = -1; - if (s_collapse < 0) { const char *cv = getenv("BT_WARP_COLLAPSE"); s_collapse = (cv && cv[0] == '1') ? 1 : 0; } - gWarpPhase = s_collapse ? 1 : 2; // 1 collapse->expand, 2 expand only - } - gWarpT = 0.0f; - gWarpPOV = pov; - gWarpX = x; gWarpY = y; gWarpZ = z; - if (getenv("BT_TLOC_LOG")) - DEBUG_STREAM << "[tloc] warp start " << (pov ? "POV(eye)" : "world") - << " at (" << x << "," << y << "," << z << ") scale=" - << gWarpCollapseScale << "/" << gWarpExpandScale << "\n" << std::flush; } -// World-anchored warp (observer sees a peer respawn over there). -void BTStartWarpEffect(float x, float y, float z) { BTKickWarp(0, x, y, z); } +// +// LOCAL DEATH -> InitialCollapse (engine: trigger becomes == control state, Idle -> +// InitialCollapse, L4VIDRND.cpp:1900-1911). The sphere collapses 100x -> 1x onto +// your own eye over 1.3s (world still visible), then raises the SetIsDead world mask +// and THROBS (WaitForReincarnate) until the respawn kicks the expand. POV only. +// +void BTStartWarpCollapsePOV() +{ + BTWarpApplyScaleEnv(); + gWarpPhase = 1; // InitialCollapse + gWarpT = 0.0f; + gWaitClock = 0.0f; + gWarpPOV = 1; + gWarpX = gWarpY = gWarpZ = 0.0f; + if (getenv("BT_TLOC_LOG")) + DEBUG_STREAM << "[tloc] warp COLLAPSE (POV) start\n" << std::flush; +} -// Eye-centred warp (the local player's OWN respawn -- the authentic POV tunnel). -void BTStartWarpEffectPOV() { BTKickWarp(1, 0.0f, 0.0f, 0.0f); } +// +// LOCAL RESPAWN -> ExpandReveal (engine: trigger becomes != control state, +// WaitForReincarnate -> ExpandReveal + SetIsDead(false), L4VIDRND.cpp:1987-1989). +// Drops the world mask and blasts the sphere 1x -> 150x, revealing the reborn world. +// +void BTStartWarpExpandPOV() +{ + BTWarpApplyScaleEnv(); + if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } // == SetIsDead(false) :1989 + gWarpPhase = 2; // ExpandReveal + gWarpT = 0.0f; + gWarpPOV = 1; + gWarpX = gWarpY = gWarpZ = 0.0f; + if (getenv("BT_TLOC_LOG")) + DEBUG_STREAM << "[tloc] warp EXPAND (POV) start\n" << std::flush; +} + +// +// World-anchored warp (a PORT EXTENSION -- the authentic effect is POV only): an +// OBSERVER seeing a peer respawn over THERE. No world mask (the observer is alive); +// expand-reveal at the peer's world point. +// +void BTStartWarpEffect(float x, float y, float z) +{ + BTWarpApplyScaleEnv(); + gWarpPhase = 2; // ExpandReveal + gWarpT = 0.0f; + gWarpPOV = 0; + gWarpX = x; gWarpY = y; gWarpZ = z; + if (getenv("BT_TLOC_LOG")) + DEBUG_STREAM << "[tloc] warp EXPAND (world) at (" << x << "," << y << "," << z << ")\n" << std::flush; +} + +// +// FAILSAFE: drop the world mask + end the effect. MUST be called on every path +// where a collapse fired but no respawn/expand can follow (mission end, out of +// lives, dropped DropZoneReply) -- otherwise the SetIsDead world stays BLACK forever. +// +void BTWarpForceUnmask() +{ + if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } + if (gWarpPhase == 3) gWarpPhase = 0; +} // // Play the warp one-shot: tsphere.bgf COLLAPSES onto the respawn point (scale @@ -2310,9 +2347,6 @@ void for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op) { L4DRAWOP *dop = gTLocSphere->GetDrawOp(op); - // Route the sphere's ops INTO the alpha-blend pass: DrawMesh skips any - // op whose alphaTest != (pass==PASS_ALPHABLEND) (L4D3D.cpp:1045). - dop->alphaTest = true; // THE SWIRL MOTION. The authentic material scrolls its texture // (tsphere_scr_tex SPECIAL "SCROLL 0.0 0.0 0.1 0.5"), but the port only // picks scroll up from a per-texture .met file (L4D3D.cpp:640), which @@ -2325,6 +2359,8 @@ void dop->texture.scrollVDelta = 0.5f; dop->texture.wrap_u = L4TEXOP::REPEAT; dop->texture.wrap_v = L4TEXOP::REPEAT; + // The pass routing (drawAsSky for POV / alphaTest for the peer overlay) + // is set per-frame below, since it differs by mode. } } if (getenv("BT_TLOC_LOG")) @@ -2344,20 +2380,47 @@ void return; } - gWarpT += dt; - - float scale; - if (gWarpPhase == 1) // collapse: (max+1) -> 1 + // ===== The POVTranslocateRenderable::Execute() state machine (L4VIDRND.cpp) ===== + float scale = 1.0f; + if (gWarpPhase == 1) // InitialCollapse (:1928): 101 -> 1 { + gWarpT += dt; float left = 1.0f - (gWarpT / TLOC_COLLAPSE_TIME); - if (left <= 0.0f) { gWarpPhase = 2; gWarpT = 0.0f; scale = 1.0f; } - else scale = left * gWarpCollapseScale + 1.0f; + if (left <= 0.0f) // collapse finished (:1935) + { + scale = 1.0f; + gWarpPhase = 3; // -> WaitForReincarnate (:1946) + gWaitClock = 0.0f; // rebaseline for the wobble (:1945) + if (gWarpPOV) { BTSetWorldDead(1); gWarpMasked = 1; } // SetIsDead(true) (:1947) + } + else + scale = left * gWarpCollapseScale + 1.0f; // (pct_left*100)+1 } - else // expand: 1 -> (max+1) + else if (gWarpPhase == 3) // WaitForReincarnate (:1978): throb black { + gWaitClock += dt; + scale = 1.0f; + // FAILSAFE: the respawn kicks Wait->Expand (BTStartWarpExpandPOV). If it never + // arrives (mission end / out of lives / dropped DropZoneReply) the SetIsDead + // world would stay BLACK forever -- so time out and un-mask after 12s. + if (gWaitClock > 12.0f) + { + if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } + gWarpPhase = 0; + return; + } + } + else // ExpandReveal (:2013): 1 -> 151 + { + gWarpT += dt; float used = gWarpT / TLOC_EXPAND_TIME; - if (used >= 1.0f) { gWarpPhase = 0; return; } // done - scale = used * gWarpExpandScale + 1.0f; + if (used >= 1.0f) // reveal done (:2030) + { + if (gWarpMasked) { BTSetWorldDead(0); gWarpMasked = 0; } // safety (mask should already be off) + gWarpPhase = 0; + return; + } + scale = used * gWarpExpandScale + 1.0f; // (pct_used*150)+1 } if (getenv("BT_TLOC_LOG")) @@ -2365,7 +2428,7 @@ void static int s_lt = 0; if ((++s_lt % 15) == 1) DEBUG_STREAM << "[tloc] warp phase=" << gWarpPhase << " scale=" << scale - << " at=(" << gWarpX << "," << gWarpY << "," << gWarpZ << ")\n" << std::flush; + << " pov=" << gWarpPOV << " masked=" << gWarpMasked << "\n" << std::flush; } // PLACEMENT (authentic POVTranslocateRenderable, L4VIDRND.cpp:1812 "rotated and @@ -2379,124 +2442,142 @@ void // - Non-POV (observing a PEER respawn): anchor at the peer's world point so the // swirl plays over there -- pure scale, then translate. const float s = scale; - // Recenter on the mesh's own bounding-sphere centre: tsphere.bgf is modelled - // with its centre OFF the origin, so a raw scale pushed it "significantly above - // the viewport" once multiplied by 100-150. Translate the centre to the origin - // FIRST, then scale, so the swirl sits ON the eye / anchor. - const D3DXVECTOR3 &cc = gTLocSphere->mCullCenter; - D3DXMATRIX recenter, scaleM; - D3DXMatrixTranslation(&recenter, -cc.x, -cc.y, -cc.z); - D3DXMatrixScaling(&scaleM, s, s, s); - D3DXMATRIX m; + // PLACEMENT -- the authentic VTV/eye parenting (L4VIDRND.cpp:2069-2074): the mesh + // LOCAL ORIGIN is the eye, so world = localToWorld * inverse(view) with NO recenter. + // tsphere is authored OFF-origin ON PURPOSE (centre +8.25y) so the eye sits ~0.38r + // low INSIDE the sphere; recentring it to dead-centre (my earlier attempt) was WRONG + // and only worsened the funnel. In WaitForReincarnate the localToWorld is a pure + // Lissajous TRANSLATION (the throb, :1996-2003); otherwise a pure SCALE (no spin -- + // myRotateY is dead; the swirl is the texture scroll). + D3DXMATRIX local; + if (gWarpPhase == 3) // WaitForReincarnate throb + D3DXMatrixTranslation(&local, + (float)(cos(gWaitClock * 3.33) * TLOC_TRANSLATE_LIMIT), + (float)(sin(gWaitClock * 2.5) * TLOC_TRANSLATE_LIMIT), 0.0f); + else + D3DXMatrixScaling(&local, s, s, s); + + D3DXMATRIX world; if (gWarpPOV && view != 0) { D3DXMATRIX invView; D3DXMatrixInverse(&invView, 0, view); - m = recenter * scaleM * invView; // centre mesh, scale, then eye->world + world = local * invView; // scale/throb about the eye, then eye->world } else { D3DXMATRIX anchorM; D3DXMatrixTranslation(&anchorM, gWarpX, gWarpY, gWarpZ); - m = recenter * scaleM * anchorM; // centre mesh, scale, move to anchor + world = local * anchorM; // peer overlay: scale about the world anchor + } + gTLocSphere->SetLocalToWorld(world); + + // The swirl COLOUR is the material's "sky" ramp remapping the grayscale bintA + // cloud per-texel (un-gated for tsphere_mtl in bgfload.cpp; L4D3D.cpp:480), so the + // bound texture is ALREADY blue-white -- MODULATE by WHITE = the ramp unchanged. + static DWORD s_warpColor = 0; + if (s_warpColor == 0) + { + const char *wc = getenv("BT_WARP_COLOR"); + s_warpColor = wc ? (DWORD)strtoul(wc, 0, 16) : 0xFFFFFFFF; + if (s_warpColor == 0) s_warpColor = 0xFFFFFFFF; } - gTLocSphere->SetLocalToWorld(m); - - // Draw the AUTHENTIC translocation-sphere material. Ground truth - // (content/VIDEO/MAT/DAY/BTFX.VMF): tsphere_mtl = TEXTURE{tsphere_scr_tex -> MAP - // bintA, SCROLL 0.0 0.0 0.1 0.5} EMISSIVE{0.7,0.5,1} RAMP{sky:(0,0,0.6)->(.99, - // .99,.99)}. The swirl is the scrolling grayscale bintA CLOUD; the blue/white is - // the "sky" RAMP remapping that cloud's luminance (deep-blue for dark texels, - // near-white for bright) -- NOT a flat tint. The port bakes exactly that ramp - // (L4D3D.cpp:480, argb=lerp(lo,hi,lum)) but had GATED it off for normal-bearing - // meshes; tsphere has normals, so it arrived RAW GRAY (why a pale tint washed to - // gray -- MODULATE can only multiply, never add the (0,0,0.6) blue floor the - // ramp does). We now un-gate the ramp for tsphere_mtl (bgfload.cpp), so the - // bound texture is ALREADY the blue-white swirl -- just draw it (MODULATE by a - // WHITE TFACTOR = no tint) and take alpha/intensity from TFACTOR. DrawMesh binds - // the ramped texture + animates the SCROLL (SetTexture/SetTextureScrolling, - // L4D3D.cpp:1093,1220); its SetTexture only rewrites the stage ops on a texturing - // on/off TRANSITION (cached mLastTexturingState, :1215) -- textured meshes drew - // first so the cache is 'on' and our MODULATE ops stand. We are in the alpha pass - // (ALPHABLENDENABLE + ZWRITE off, L4VIDEO.cpp:7762) but the blend/colour are - // indeterminate here (beams/pfx drew first): set them explicitly + RESTORE - // every state (a leaked LIGHTING/TFACTOR/stage-op corrupts the scene). - DWORD sSrc, sDst, sLight, sTFactor, sCOp, sCA1, sCA2, sAOp, sAA1, sZ; - device->GetRenderState(D3DRS_ZENABLE, &sZ); - device->GetRenderState(D3DRS_SRCBLEND, &sSrc); - device->GetRenderState(D3DRS_DESTBLEND, &sDst); - device->GetRenderState(D3DRS_LIGHTING, &sLight); - device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTFactor); - device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp); - device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1); - device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2); - device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp); - device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1); - - // Blend: authentic translucency (SRCALPHA/INVSRCALPHA) by DEFAULT. When the - // sphere expands past the respawning mech's own camera it ENGULFS the eye, and - // with additive blending the near + far sphere walls stack and SATURATE to a - // flat tint -> the "purple blob from my own perspective" the user saw (from a - // distance the sphere is small on-screen so additive stayed subtle and the swirl - // read fine). Translucent blend lets the walls blend instead of accumulate, so - // the scrolling swirl shows through even when the eye is inside the sphere. - // BT_WARP_ADDITIVE=1 restores the additive glow (punchier at a distance). - device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + if (gWarpPOV) { - static int s_additive = -1; - if (s_additive < 0) + // ===== POV: draw as SKY, exactly like the engine (isDeathDraw -> drawAsSky, + // L4VIDRND.cpp:1802-1808). OPAQUE + backface-cull (CW) + z-test ON is what makes + // an inside-viewed dome render CLEAN: no translucent double-blend, no coincident + // double-winding z-fight (that WAS the "glitchy funnel"), and the huge expand + // shell is occluded by the world coming back on = the reveal. The world itself + // is blacked by the SetIsDead mask (raised at collapse-end), so during the throb + // you see ONLY the swirling dome. Sky-pass states: L4VIDEO.cpp:7526 (cull CW), + // 7568-7570 (zwrite on / blend off), 7693 (light off). Leave ZENABLE default. ===== + for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op) { - const char *a = getenv("BT_WARP_ADDITIVE"); - s_additive = (a && a[0] == '1') ? 1 : 0; - } - device->SetRenderState(D3DRS_DESTBLEND, - s_additive ? D3DBLEND_ONE : D3DBLEND_INVSRCALPHA); - } - device->SetRenderState(D3DRS_LIGHTING, FALSE); - // Z-TEST OFF: the sphere is huge (up to 150x) and this arena is an enclosed - // CAVERN, so a depth-tested sphere gets CLIPPED by the ceiling/walls ("the cavern - // is obscuring it"). The warp is a POV overlay that engulfs you -- draw it over - // the world regardless of depth (ZWRITE is already off in the alpha pass, so it - // does not pollute the depth buffer for later draws). BT_WARP_ZTEST=1 keeps the - // depth test if the overlay-over-everything look is unwanted. - { - static int s_ztest = -1; - if (s_ztest < 0) { const char *z = getenv("BT_WARP_ZTEST"); s_ztest = (z && z[0] == '1') ? 1 : 0; } - if (!s_ztest) device->SetRenderState(D3DRS_ZENABLE, FALSE); - } - { - // TFACTOR: the RAMP now supplies the blue-white, so the tint is WHITE by - // default (MODULATE by white = the baked swirl unchanged); only the ALPHA - // byte matters -- ~70% so the swirl reads AND you see through it. - // BT_WARP_COLOR=AARRGGBB (hex) overrides (e.g. to push the hue or opacity). - static DWORD s_warpColor = 0; - if (s_warpColor == 0) - { - const char *wc = getenv("BT_WARP_COLOR"); - s_warpColor = wc ? (DWORD)strtoul(wc, 0, 16) : 0xB0FFFFFF; - if (s_warpColor == 0) s_warpColor = 0xB0FFFFFF; + L4DRAWOP *dop = gTLocSphere->GetDrawOp(op); + dop->drawAsSky = true; dop->alphaTest = false; dop->drawAsDecal = false; } + DWORD sZW, sAB, sCull, sLight, sTF, sCOp, sCA1, sCA2, sAOp, sAA1; + device->GetRenderState(D3DRS_ZWRITEENABLE, &sZW); + device->GetRenderState(D3DRS_ALPHABLENDENABLE, &sAB); + device->GetRenderState(D3DRS_CULLMODE, &sCull); + device->GetRenderState(D3DRS_LIGHTING, &sLight); + device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTF); + device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp); + device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1); + device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2); + device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp); + device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1); + + device->SetRenderState(D3DRS_ZWRITEENABLE, TRUE); + device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); // OPAQUE -> single write/pixel + device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // one winding -> no z-fight + device->SetRenderState(D3DRS_LIGHTING, FALSE); device->SetRenderState(D3DRS_TEXTUREFACTOR, s_warpColor); + device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); + device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // ramped blue-white swirl + device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR); + device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); + device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); + + gTLocSphere->Draw(PASS_SKY, view, frame_time); + + device->SetRenderState(D3DRS_ZWRITEENABLE, sZW); + device->SetRenderState(D3DRS_ALPHABLENDENABLE, sAB); + device->SetRenderState(D3DRS_CULLMODE, sCull); + device->SetRenderState(D3DRS_LIGHTING, sLight); + device->SetRenderState(D3DRS_TEXTUREFACTOR, sTF); + device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp); + device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1); + device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2); + device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp); + device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1); } - device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); - device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); // ramped blue-white swirl - device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR); // white (no tint) unless overridden - device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); - device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); // uniform ~70% translucency + else + { + // ===== Peer overlay (port extension): the observer is ALIVE (no world mask), so + // a sky-drawn sphere would be occluded by the world -> invisible. Draw it in the + // alpha pass, Z-tested + translucent, so the swirl reads at the peer's spot. ===== + for (int op = 0; op < gTLocSphere->GetDrawOpCount(); ++op) + { + L4DRAWOP *dop = gTLocSphere->GetDrawOp(op); + dop->alphaTest = true; dop->drawAsSky = false; dop->drawAsDecal = false; + } + const DWORD peerColor = (s_warpColor == 0xFFFFFFFF) ? 0xB0FFFFFF : s_warpColor; // ~70% alpha + DWORD sSrc, sDst, sLight, sTF, sCOp, sCA1, sCA2, sAOp, sAA1; + device->GetRenderState(D3DRS_SRCBLEND, &sSrc); + device->GetRenderState(D3DRS_DESTBLEND, &sDst); + device->GetRenderState(D3DRS_LIGHTING, &sLight); + device->GetRenderState(D3DRS_TEXTUREFACTOR, &sTF); + device->GetTextureStageState(0, D3DTSS_COLOROP, &sCOp); + device->GetTextureStageState(0, D3DTSS_COLORARG1, &sCA1); + device->GetTextureStageState(0, D3DTSS_COLORARG2, &sCA2); + device->GetTextureStageState(0, D3DTSS_ALPHAOP, &sAOp); + device->GetTextureStageState(0, D3DTSS_ALPHAARG1, &sAA1); - gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time); + device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + device->SetRenderState(D3DRS_LIGHTING, FALSE); + device->SetRenderState(D3DRS_TEXTUREFACTOR, peerColor); + device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); + device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); + device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR); + device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); + device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TFACTOR); - device->SetRenderState(D3DRS_ZENABLE, sZ); - device->SetRenderState(D3DRS_SRCBLEND, sSrc); - device->SetRenderState(D3DRS_DESTBLEND, sDst); - device->SetRenderState(D3DRS_LIGHTING, sLight); - device->SetRenderState(D3DRS_TEXTUREFACTOR, sTFactor); - device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp); - device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1); - device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2); - device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp); - device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1); + gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time); + + device->SetRenderState(D3DRS_SRCBLEND, sSrc); + device->SetRenderState(D3DRS_DESTBLEND, sDst); + device->SetRenderState(D3DRS_LIGHTING, sLight); + device->SetRenderState(D3DRS_TEXTUREFACTOR, sTF); + device->SetTextureStageState(0, D3DTSS_COLOROP, sCOp); + device->SetTextureStageState(0, D3DTSS_COLORARG1, sCA1); + device->SetTextureStageState(0, D3DTSS_COLORARG2, sCA2); + device->SetTextureStageState(0, D3DTSS_ALPHAOP, sAOp); + device->SetTextureStageState(0, D3DTSS_ALPHAARG1, sAA1); + } } //===========================================================================// diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 30a4462..56dafed 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -314,6 +314,10 @@ void // if (GetSimulationState() == MissionEndingState) { + // If our death raised the warp world-mask but the mission is tearing down + // before we respawn, drop it now so the world can never stay black. + if (this == (BTPlayer *)application->GetMissionPlayer()) + { extern void BTWarpForceUnmask(); BTWarpForceUnmask(); } Check_Fpu(); return; } @@ -357,6 +361,10 @@ void if (entity_manager == 0 || entity_manager->FindGroup("DropZones") == 0) { + // No respawn possible -> the expand will never fire -> drop the warp + // world-mask now so we don't stay black waiting for a reincarnation. + if (this == (BTPlayer *)application->GetMissionPlayer()) + { extern void BTWarpForceUnmask(); BTWarpForceUnmask(); } if (getenv("BT_SCORE_LOG")) DEBUG_STREAM << "[score] VehicleDead: no DropZones group -- " "respawn unavailable in this mission\n" << std::flush; @@ -386,6 +394,16 @@ void } deathPending = 1; // this+0x290 + // WARP (task #52): our OWN death = the engine's Idle -> InitialCollapse (trigger + // becomes == control state, POVTranslocateRenderable). Collapse the eye-centred + // sphere 100x -> 1x, then it raises the SetIsDead world mask and throbs until the + // respawn (DropZoneReply) releases the expand-reveal. LOCAL player only (POV). + if (this == (BTPlayer *)application->GetMissionPlayer()) + { + extern void BTStartWarpCollapsePOV(); + BTStartWarpCollapsePOV(); + } + if (getenv("BT_SCORE_LOG")) DEBUG_STREAM << "[score] VehicleDead(-1): death #" << (deathCount + 1) << ", respawn hunt in 5s\n" << std::flush; @@ -1142,14 +1160,17 @@ void Mech *mech = (Mech *)playerVehicle; mech->Reset(message->dropZoneLocation, 1 /* True */); // FUN_0049fb74 (heal+move) - // WARP (task #52): the translocation sphere at our reinsertion. This is OUR - // OWN respawn, so use the POV variant -- the sphere centres on and orients to - // our eye (the authentic VTV-parented POVTranslocateRenderable), collapsing - // through us and blasting back open. A self-contained render one-shot -- it - // does NOT touch SimulationState (that dial drives the camera/POV + targeting; - // pulsing it regressed all of those). - extern void BTStartWarpEffectPOV(); - BTStartWarpEffectPOV(); + // WARP (task #52): our OWN respawn = the engine's WaitForReincarnate -> Expand + // release (POVTranslocateRenderable): drop the SetIsDead world mask and blast + // the eye-centred sphere open to reveal the reborn world. LOCAL player only (a + // POV effect); a self-contained render one-shot that does NOT touch + // SimulationState (that dial drives the camera/POV + targeting -- pulsing it + // regressed all of those; SetIsDead is a separate pure-render flag). + if (this == (BTPlayer *)application->GetMissionPlayer()) + { + extern void BTStartWarpExpandPOV(); + BTStartWarpExpandPOV(); + } } Check_Fpu(); }