Implement the translocation sphere -- the "blue warp" respawn effect (task #52)

BTTranslocationRenderable was a no-op stub; now reconstructed over the engine's
POVTranslocateRenderable: loads tsphere.bgf and runs a collapse-on-arrival /
expand-on-reveal sphere (scale 100->1 over 1.3s, then 1->150 over 1.0s, rotating)
keyed on the player's SimulationState dial. Drawn direct from the render loop by
BTDrawTranslocationSpheres (beside BTDrawBeams, PASS_ALPHABLEND) -- the same
accommodation the beams/reticle use.

The asset loads by FILENAME (tsphere.bgf), not through the RES table -- which is
why every resource-name search missed the effect for three rounds.

Trigger wiring: btl4vid.cpp MakeEntityRenderables builds the sphere for the LOCAL
player (the authentic wiring builds it only for replicants + a POV fade for self,
but peer player-attribute replication isn't wired -- on a replicant SimulationState/
DropZoneLocation read uninitialised). btplayer.cpp pulses SimulationState
DropZoneAcquired->VehicleTranslocated at respawn (a 1.4s flip timer stands in for
the engine's +1s drop-zone re-post) and writes the respawn origin into the
DropZoneLocation attribute.

Verified 2-node (BT_TLOC_LOG): on respawn the sphere collapses (~100->5) then
expands (->150) at the valid drop-zone origin, deduped to <=2 active, respawn
cycle un-regressed, no render errors. Visual appearance (colour/size) still needs
a live look; the authentic see-others'-spheres path needs player replication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 21:41:57 -05:00
co-authored by Claude Opus 4.8
parent 6cd5f0b940
commit 63c1c5a460
6 changed files with 290 additions and 8 deletions
+200
View File
@@ -197,6 +197,24 @@ void
dplMainZone, dplDeathZone, sim_state,
fogRed, fogGreen, fogBlue, fogNear, fogFar,
3 /* MissionStartingState */, 4 /* MissionEndingState */);
//
// BRING-UP (task #52): ALSO give the LOCAL player the translocation
// SPHERE (the authentic wiring shows it only on OTHERS -- replicant
// branch above -- because peer player-attribute replication of
// SimulationState/DropZoneLocation isn't wired yet; both read
// uninitialised on a replicant). Driven by the local player's OWN
// SimulationState dial (pulsed to DropZoneAcquired->Translocated at
// respawn, btplayer.cpp) at its own DropZoneLocation -- so you see
// the warp collapse+expand around your reinsertion. Same control
// state (1 = DropZoneAcquired). Remove once player replication lands.
//
Point3D *local_drop =
(Point3D *)entity->GetAttributePointer("DropZoneLocation");
if (local_drop)
new BTTranslocationRenderable(
entity, VideoRenderable::Watcher, GetMainView(),
sim_state, local_drop, 1);
}
break;
}
@@ -2088,3 +2106,185 @@ Logical
}
//===========================================================================//
// BTTranslocationRenderable -- the "blue warp" translocation sphere (task #52)
//
// Reconstructed from the engine's POVTranslocateRenderable (L4VIDRND.cpp:1749):
// a sphere (tsphere.bgf) that COLLAPSES onto the mech when the watched player
// enters the control state (DropZoneAcquired), holds while dead, then EXPANDS
// to reveal the reborn mech when the state leaves it (VehicleTranslocated).
// Rotates throughout. Loaded by FILENAME (not via the RES table) -- which is
// why every resource-name search missed it. Drawn direct from the render loop
// (BTDrawTranslocationSpheres, called beside BTDrawBeams), same accommodation
// the weapon beams + reticle use since our VideoComponent tree is partial.
//===========================================================================//
namespace {
// Timings + scales are the engine's #defines (L4VIDRND.cpp:1765-1772).
const float TLOC_COLLAPSE_TIME = 1.3f;
const float TLOC_COLLAPSE_START_SCALE = 100.0f;
const float TLOC_EXPAND_TIME = 1.0f;
const float TLOC_EXPAND_END_SCALE = 150.0f;
const float TLOC_ROTATE_RATE = 0.5f * 0.01745329222222f; // rad/frame
BTTranslocationRenderable *gTLocFx[64];
int gTLocFxCount = 0;
d3d_OBJECT *gTLocSphere = 0;
int gTLocSphereTried = 0;
}
BTTranslocationRenderable::BTTranslocationRenderable(
Entity *entity, int /*execution_type*/, dpl_VIEW * /*this_view*/,
StateIndicator *effect_trigger, Point3D *drop_zone, int effect_control_state)
: BTRenderableBase(entity),
myWatchedEntity(entity),
myTrigger(effect_trigger),
myDropZone(drop_zone),
myControlState((unsigned)effect_control_state),
mySphereState(TLoc_Idle),
myTimer(0.0f),
myRotateY(0.0f),
mySphereVisible(false)
{
// A player's render tree is rebuilt on respawn, so drop any prior (now
// stale) sphere for the SAME entity from the active walk -- only the newest
// steps + draws (the old renderable objects are owned/freed by their tree).
for (int i = 0; i < gTLocFxCount; ++i)
if (gTLocFx[i] != 0 && gTLocFx[i]->myWatchedEntity == entity)
{
gTLocFx[i] = gTLocFx[--gTLocFxCount];
--i;
}
if (gTLocFxCount < 64)
gTLocFx[gTLocFxCount++] = this;
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] renderable created for entity " << entity->GetEntityID()
<< " control=" << myControlState << " (active=" << gTLocFxCount << ")\n" << std::flush;
}
BTTranslocationRenderable::~BTTranslocationRenderable()
{
for (int i = 0; i < gTLocFxCount; ++i)
if (gTLocFx[i] == this)
{
gTLocFx[i] = gTLocFx[--gTLocFxCount];
break;
}
}
//
// Step every active translocation sphere's state machine and draw the visible
// ones. Called from the render loop (L4VIDEO.cpp) with the frame's view matrix
// + dt, so the sphere occludes against the depth already laid down by the world.
//
void
BTDrawTranslocationSpheres(LPDIRECT3DDEVICE9 device, const D3DXMATRIX *view,
float dt, Time frame_time)
{
if (gTLocFxCount == 0)
return;
if (gTLocSphere == 0 && !gTLocSphereTried)
{
gTLocSphereTried = 1;
gTLocSphere = d3d_OBJECT::LoadObject(device, "tsphere.bgf");
if (getenv("BT_TLOC_LOG"))
DEBUG_STREAM << "[tloc] tsphere.bgf load "
<< (gTLocSphere ? "OK" : "FAILED") << "\n" << std::flush;
}
if (gTLocSphere == 0)
return;
const int log_on = getenv("BT_TLOC_LOG") ? 1 : 0;
static int s_logTick = 0;
const int log_now = log_on && ((++s_logTick % 30) == 1);
for (int i = 0; i < gTLocFxCount; ++i)
{
BTTranslocationRenderable *fx = gTLocFx[i];
if (fx == 0 || fx->myTrigger == 0 || fx->myDropZone == 0)
continue;
const unsigned st = fx->myTrigger->GetState();
float scale = 1.0f;
switch (fx->mySphereState)
{
case BTTranslocationRenderable::TLoc_Idle:
if (st == fx->myControlState)
{
fx->mySphereState = BTTranslocationRenderable::TLoc_Collapse;
fx->myTimer = 0.0f;
fx->mySphereVisible = true;
}
break;
case BTTranslocationRenderable::TLoc_Collapse:
{
fx->myTimer += dt;
float left = 1.0f - (fx->myTimer / TLOC_COLLAPSE_TIME); // 1 -> 0
if (left <= 0.0f)
{
scale = 1.0f;
fx->mySphereState = BTTranslocationRenderable::TLoc_Wait;
fx->myTimer = 0.0f;
}
else
scale = left * TLOC_COLLAPSE_START_SCALE + 1.0f; // 101 -> 1
break;
}
case BTTranslocationRenderable::TLoc_Wait:
scale = 1.0f;
if (st != fx->myControlState) // respawned/translocated
{
fx->mySphereState = BTTranslocationRenderable::TLoc_Expand;
fx->myTimer = 0.0f;
}
break;
case BTTranslocationRenderable::TLoc_Expand:
{
fx->myTimer += dt;
float used = fx->myTimer / TLOC_EXPAND_TIME; // 0 -> 1
if (used >= 1.0f)
{
scale = 1.0f;
fx->mySphereState = BTTranslocationRenderable::TLoc_Idle;
fx->mySphereVisible = false;
}
else
scale = used * TLOC_EXPAND_END_SCALE + 1.0f; // 1 -> 151
break;
}
}
if (log_now)
DEBUG_STREAM << "[tloc] fx=" << i << " trigger=" << st
<< " ctrl=" << fx->myControlState << " state=" << fx->mySphereState
<< " scale=" << scale << " vis=" << (int)fx->mySphereVisible
<< " at=(" << fx->myDropZone->x << "," << fx->myDropZone->y
<< "," << fx->myDropZone->z << ")\n" << std::flush;
if (!fx->mySphereVisible)
continue;
fx->myRotateY += TLOC_ROTATE_RATE;
// world = scale * rotateY(myRotateY), translated to the drop zone.
const float s = scale;
const float c = cosf(fx->myRotateY);
const float sn = sinf(fx->myRotateY);
D3DXMATRIX m;
m._11 = s * c; m._12 = 0.0f; m._13 = -s * sn; m._14 = 0.0f;
m._21 = 0.0f; m._22 = s; m._23 = 0.0f; m._24 = 0.0f;
m._31 = s * sn; m._32 = 0.0f; m._33 = s * c; m._34 = 0.0f;
m._41 = (float)fx->myDropZone->x;
m._42 = (float)fx->myDropZone->y;
m._43 = (float)fx->myDropZone->z;
m._44 = 1.0f;
gTLocSphere->SetLocalToWorld(m);
gTLocSphere->Draw(PASS_ALPHABLEND, view, frame_time);
}
}
//===========================================================================//
+25
View File
@@ -317,6 +317,16 @@ class BTMarkerWatcherRenderable:
//
// Drop-zone translocation effect renderable (FUN_00458d2c, alloc 0x40).
//
// THE "BLUE WARP" (task #52): the engine analog is POVTranslocateRenderable
// (L4VIDRND.cpp:1749), which loads tsphere.bgf and runs a collapse-on-death /
// expand-on-respawn sphere keyed on the entity's SimulationState dial vs a
// control state. This was a no-op stub; now reconstructed. The sphere
// COLLAPSES (scale 100->1 over 1.3s) when the watched player enters the
// control state (DropZoneAcquired), holds, then EXPANDS (1->150 over 1.0s)
// when it leaves (VehicleTranslocated) -- revealing the reborn mech. Rotates
// throughout. Drawn (direct-draw, like the weapon beams) by
// BTDrawTranslocationSpheres from the render loop.
//
class BTTranslocationRenderable:
public BTRenderableBase
{
@@ -328,6 +338,21 @@ class BTTranslocationRenderable:
StateIndicator *effect_trigger,
Point3D *drop_zone,
int effect_control_state);
~BTTranslocationRenderable();
// Sphere state machine (public so the file-static render walk in
// btl4vid.cpp can step + draw each active effect). Mirrors
// POVTranslocateRenderable's IdleState/InitialCollapse/WaitForReincarnate/
// ExpandReveal.
enum { TLoc_Idle = 0, TLoc_Collapse, TLoc_Wait, TLoc_Expand };
Entity *myWatchedEntity; // the player this sphere belongs to (dedupe key)
StateIndicator *myTrigger; // the SimulationState dial we watch
Point3D *myDropZone; // world position to render the sphere at
unsigned myControlState; // state value that starts the collapse
int mySphereState; // TLoc_*
float myTimer; // seconds elapsed in the current timed phase
float myRotateY; // accumulated spin
bool mySphereVisible;
};
//
+37
View File
@@ -204,6 +204,11 @@ static_assert(sizeof(BTPlayer::MakeMessage) == 0xD0,
static const char *SelfDestructName = "self destruct"; // &DAT_00524b38
static const Scalar RespawnDelay = 5.0f; // death -> drop-zone hunt (@004c0830)
// WARP (task #52): countdown from the respawn's DropZoneAcquired pulse to the
// VehicleTranslocated flip that makes the translocation sphere EXPAND (reveal).
// One local player per node, so a file-static holds it. <0 = idle.
static Scalar sWarpExpandTimer = -1.0f;
static const Scalar TicksPerSecond = 1.0f; // (see note in PlayerSimulation)
//
@@ -754,6 +759,19 @@ void
Player::PlayerSimulation(time_slice); // FUN_0042e168
// WARP (task #52): once the collapse has had time to play, flip the
// SimulationState dial off DropZoneAcquired so the translocation sphere
// leaves its "wait" phase and EXPANDS (reveals the reborn mech).
if (sWarpExpandTimer > 0.0f)
{
sWarpExpandTimer -= time_slice;
if (sWarpExpandTimer <= 0.0f)
{
sWarpExpandTimer = -1.0f;
SetSimulationState(VehicleTranslocatedState); // state 2 -> expand
}
}
if (
(lastPerformance - lastConsoleUpdate) / TicksPerSecond >= CONSOLE_UPDATE_INTERVAL // _DAT_004c08fc
|| application->GetApplicationState() == Application::EndingMission // app+0x88 == 6
@@ -1047,6 +1065,25 @@ void
AlwaysExecute(); // param_1[10] &= ~2 (run every frame)
deathCount = 0; // param_1[0x80]
//
// WARP (task #52): fire the translocation sphere at our reinsertion.
// Publish the drop-zone position into our DropZoneLocation attribute (the
// sphere renderable draws there) and pulse the SimulationState dial to
// DropZoneAcquired -- state 1, the sphere's COLLAPSE trigger.
// PlayerSimulation flips it to VehicleTranslocated ~1.4 s later, which the
// sphere reads as "reincarnate" and EXPANDS to reveal the reborn mech.
// (Authentic trigger = the player's own SimulationState + DropZoneLocation
// attribute; the flip-timer stands in for the engine's +1 s drop-zone
// re-post choreography, which our sever-and-recreate respawn skips.)
//
{
Point3D *drop_attr = (Point3D *)GetAttributePointer("DropZoneLocation");
if (drop_attr != 0)
*drop_attr = message->dropZoneLocation.linearPosition;
}
SetSimulationState(DropZoneAcquiredState); // state 1 -> collapse
sWarpExpandTimer = 1.4f;
}
else if (deathCount == message->deathCount) // param_2[0xe] == param_1[0x80]
{
+2 -3
View File
@@ -362,9 +362,8 @@ BTMarkerWatcherRenderable::BTMarkerWatcherRenderable(
Entity *entity, int, dpl_VIEW *, dpl_DCS *)
: BTRenderableBase(entity) {}
BTTranslocationRenderable::BTTranslocationRenderable(
Entity *entity, int, dpl_VIEW *, StateIndicator *, Point3D *, int)
: BTRenderableBase(entity) {}
// BTTranslocationRenderable (the "blue warp" translocation sphere) is now a real
// reconstruction in btl4vid.cpp (task #52) -- no longer a stub here.
BTPOVStartEndRenderable::BTPOVStartEndRenderable(
Entity *entity, int, dpl_VIEW *, dpl_ZONE *, dpl_ZONE *, StateIndicator *,