Wreck sink (quadratic burial) + ldbr debris field -- the 1996 script completed

Answers "does the wreck fade away?": YES, by sinking.  FUN_00456410 (the 1996
sink renderable) computes offsetY = rate * t^2; the hulk's authored rate is
-0.025 -> the ~7-unit hulk is fully underground ~17s after the kill.  The
script also pairs the standing hulk with the LDBR strewn-debris field (12x13u
flat scatter), parented together and sinking together.

Also verified from the mesh data: BLHDBR (1537 verts -- more than the intact
torso) IS the authored Blackhawk wreck: the classic standing-leg-in-rubble
sculpt.  The "just a leg standing there" report is the authentic art.
(THRDBR -- the mesh the 1996 script hardcoded -- parses to ZERO vertices;
more evidence the hardcode was an unfinished dev shortcut.)

Implementation: SwapToWreck adds the ldbr piece; TickWreck applies the
quadratic sink per frame (driven from the dead mech's UpdateDeathState),
hides both pieces at burial and reports it so the wreck-smoke re-arm stops
with the wreck.  DPLStaticChildRenderable::SetOffsetTranslation added
(Execute re-reads OrientationMatrix per frame -- same in-place idiom as
SetDrawObj).

Lifecycle verified live: kill -> 'blhdbr.bgf' + ldbr debris -> smoke re-arm
@10s -> wreck buried @~17s -> smoke stops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 15:55:01 -05:00
co-authored by Claude Fable 5
parent c0fa6bf21a
commit 5236d4263a
5 changed files with 166 additions and 9 deletions
+10
View File
@@ -1900,6 +1900,16 @@ public:
Logical TestInstance() const;
virtual void Execute();
// Overwrite the offset's translation row in place. Execute() re-reads
// OrientationMatrix every frame, so this animates the child -- the BT
// death-wreck SINK (the 1996 script's quadratic burial) drives it.
void SetOffsetTranslation(float x, float y, float z)
{
OrientationMatrix(3, 0) = x;
OrientationMatrix(3, 1) = y;
OrientationMatrix(3, 2) = z;
}
protected:
dpl_DCS *myDCS, *myParentDCS;
+80 -8
View File
@@ -796,9 +796,15 @@ void
}
//
// Hide the body; hang the hulk on the tree root (identity offset -- the
// root renderable already pushes the wreck's localToWorld, so the hulk
// sits at the mech's ground position with its death yaw).
// The strewn-debris field that accompanies the standing hulk (the 1996
// script pairs them: thrdbr + ldbr, parented together, sinking together).
//
d3d_OBJECT *debris = d3d_OBJECT::LoadObject(GetDevice(), "ldbr.bgf");
//
// Hide the body; hang the wreck pieces on the tree root (identity offset --
// the root renderable already pushes the wreck's localToWorld, so they sit
// at the mech's ground position with its death yaw).
//
for (std::map<int, HierarchicalDrawComponent*>::iterator r =
render_tree.segRenderable.begin();
@@ -807,22 +813,88 @@ void
if (r->second != NULL)
r->second->SetDrawObj(NULL);
}
if (hulk != NULL && render_tree.rootRenderable != NULL)
if (render_tree.rootRenderable != NULL)
{
dpl_ISECT_MODE isect_mode;
LinearMatrix identity(True);
new DPLStaticChildRenderable(
victim, false /* main zone */, hulk,
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
if (hulk != NULL)
render_tree.wreckHulk = new DPLStaticChildRenderable(
victim, false /* main zone */, hulk,
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
if (debris != NULL)
render_tree.wreckDebris = new DPLStaticChildRenderable(
victim, false /* main zone */, debris,
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
}
render_tree.wrecked = 1;
render_tree.wrecked = 1;
render_tree.wreckAge = 0.0f;
DEBUG_STREAM << "[BTrender] wreck swap: victim -> '"
<< (hulk_name[0] ? hulk_name : "gendbr.bgf")
<< (hulk ? "'" : "' (LOAD FAILED -- body hidden only)")
<< (debris ? " + ldbr debris" : "")
<< "\n" << std::flush;
}
//
//#############################################################################
// TickWreck -- the wreck's quadratic SINK (the 1996 burial)
//#############################################################################
//
// FUN_00456410 (the 1996 sink renderable): offsetY = rate * t^2, hulk rate
// -0.025 (armed 0.25s after the boom by a sweep trigger). The ~7-unit hulk is
// fully underground ~17s after the kill -- the wreck visual "fades away" by
// burial; the ENTITY (sim/collision) stays, per the wreck-stays rule. Once
// buried, the pieces are hidden and the sink stops.
//
int
BTL4VideoRenderer::TickWreck(Entity *victim, float dt)
{
std::map<Entity*, MechRenderTree>::iterator tree_it =
mMechRenderTrees.find(victim);
if (tree_it == mMechRenderTrees.end())
return 1; // no tree yet -- not buried
MechRenderTree &render_tree = tree_it->second;
if (!render_tree.wrecked)
return 1; // not swapped yet
if (render_tree.wreckHulk == NULL && render_tree.wreckDebris == NULL)
return 0; // already buried
render_tree.wreckAge += dt;
float sink = -0.025f * render_tree.wreckAge * render_tree.wreckAge; // the authored rate
if (sink < -8.0f)
{
// fully buried -> hide + stop ticking
if (render_tree.wreckHulk) render_tree.wreckHulk->SetDrawObj(NULL);
if (render_tree.wreckDebris) render_tree.wreckDebris->SetDrawObj(NULL);
render_tree.wreckHulk = NULL;
render_tree.wreckDebris = NULL;
DEBUG_STREAM << "[BTrender] wreck buried (sink complete)\n" << std::flush;
return 0;
}
if (render_tree.wreckHulk)
render_tree.wreckHulk->SetOffsetTranslation(0.0f, sink, 0.0f);
if (render_tree.wreckDebris)
render_tree.wreckDebris->SetOffsetTranslation(0.0f, sink, 0.0f);
return 1;
}
//
// Sim-side bridge (UpdateDeathState drives the sink each dead frame).
//
int BTWreckSinkTick(Entity *victim, float dt)
{
if (victim == NULL || application == NULL)
return 1;
BTL4VideoRenderer *renderer =
(BTL4VideoRenderer *)application->GetVideoRenderer();
if (renderer == NULL)
return 1;
return renderer->TickWreck(victim, dt);
}
//
// Engine-side bridge (the ExplosionClassID dispatch calls this on effect 104).
//
+22
View File
@@ -566,11 +566,26 @@ class BTReticleRenderable:
int skeletonType; // EntitySegment::SkeletonType used at build
HierarchicalDrawComponent *rootRenderable;// the tree root (wreck hulk parent)
int wrecked; // 1 = swapped to the <mech>dbr hulk
DPLStaticChildRenderable *wreckHulk; // the <mech>dbr piece (sinks)
DPLStaticChildRenderable *wreckDebris; // the ldbr scatter (sinks)
float wreckAge; // seconds since the swap
std::map<int, HierarchicalDrawComponent*> segRenderable; // slot -> joint renderable
std::map<int, int> segGState; // slot -> last applied graphic state
};
std::map<Entity*, MechRenderTree> mMechRenderTrees;
public:
//
// The wreck's quadratic SINK (the 1996 script's burial: FUN_00456410
// computes offsetY = rate * t^2; the hulk's authored rate is -0.025 ->
// the ~7-unit hulk is fully buried ~17s after the kill). Ticked per
// frame from the dead mech's UpdateDeathState. Returns 0 once the
// wreck is fully buried (the caller stops the wreck-smoke re-arm),
// 1 while anything is still visible.
//
int
TickWreck(Entity *victim, float dt);
public:
//
// The death-wreck swap (ExplosionScripts effect 104, reconstructed): the
@@ -661,6 +676,13 @@ extern void BTRemakeMechModel(Entity *entity);
//
extern void BTSwapMechToWreck(Entity *victim);
//
// Per-frame wreck sink tick (sim -> render bridge; called by the dead mech's
// UpdateDeathState with the frame dt). Returns 0 once the wreck is fully
// buried -- the caller stops re-arming the wreck smoke.
//
extern int BTWreckSinkTick(Entity *victim, float dt);
#endif // BTL4VID_HPP
//===========================================================================//
+7 -1
View File
@@ -955,8 +955,14 @@ void
// The .PFX itself trickles 3 particles/sec for 10s; re-arming it on that
// same cadence reads as a continuously burning wreck. The one-shot death
// visuals (dnboom + skins) stay in the kill path.
// The wreck hulk's quadratic SINK (the 1996 script's burial): the pieces
// settle into the ground and are gone ~17s after the kill. While the
// wreck is still visible, keep the death/rubble smoke plume alive on its
// authored 10s window; once buried, the smoke stops with it.
extern int BTWreckSinkTick(Entity *victim, float dt);
int wreck_visible = BTWreckSinkTick(this, (float)dt);
wreckSmokeTimer -= dt;
if (wreckSmokeTimer <= 0.0f)
if (wreck_visible && wreckSmokeTimer <= 0.0f)
{
wreckSmokeTimer = 10.0f; // DDTHSMK's release window
extern void BTStartPfx(int effect_number, float x, float y, float z);
+47
View File
@@ -0,0 +1,47 @@
import struct, sys
VTX_TAGS = {0x80: 12, 0x81: 24, 0x82: 28, 0x88: 20, 0x89: 32, 0x8A: 36}
CONTAINERS = {0x03, 0x40, 0x41, 0x42, 0x46, 0x48, 0x70} # HEADER OBJECT LOD PATCH PMESH SPHERE_LIST BOUND
def bounds(path):
data = open(path, 'rb').read()
assert data[:8] == b'DIV-BIZ2', path
lo = [1e9, 1e9, 1e9]
hi = [-1e9, -1e9, -1e9]
nverts = 0
def walk(pos, end):
nonlocal nverts
while pos < end - 2:
tagword, = struct.unpack_from('<H', data, pos)
tag = tagword & 0x2fff
wide = (tagword >> 14) & 3
if wide == 1:
ln, = struct.unpack_from('<H', data, pos + 2)
hdr = 4
else:
ln = data[pos + 2]
hdr = 3
payload = pos + hdr
if tag in VTX_TAGS:
stride = VTX_TAGS[tag]
n = ln // stride
for i in range(n):
x, y, z = struct.unpack_from('<3f', data, payload + i * stride)
for a, v in enumerate((x, y, z)):
if v < lo[a]: lo[a] = v
if v > hi[a]: hi[a] = v
nverts += n
elif tag in CONTAINERS:
walk(payload, payload + ln)
pos = payload + ln
walk(8, len(data))
print('%-14s verts=%5d X[%7.2f %7.2f] Y[%7.2f %7.2f] Z[%7.2f %7.2f]'
% (path.split('\\')[-1], nverts, lo[0], hi[0], lo[1], hi[1], lo[2], hi[2]))
base = r'C:\git\bt411\content\VIDEO\GEO\%s'
for f in ['LDBR.BGF', 'MDBR.BGF', 'FLAMESML.BGF', 'FLAMEBIG.BGF']:
try:
bounds(base % f)
except Exception as e:
print(f, 'ERR', e)