Fire VISUALS wave: the authored firesmoke sheet, vertex-alpha effect cards, the case-4 wreck dressing
The "fireballs like the demo vids" arc, decomp/content-grounded end to end,
plus the live-play UX batch verified over the same sessions:
FIRESMOKE SHEET (the PFX fireball fix): every firesmokeN_scr_tex in BTFX.VMF
maps the SAME 64x64 tileable noise image bintA (variants differ only in SCROLL
rate) and firesmoke1_mtl colours it through the "fiery" ramp (0.3,0.1,0.1)->
(0.9,0.7,0.3). The particle layer now bakes ramp(lum(bintA)) as its sprite
colour (noise detail in alpha) and SCROLLS it at firesmoke1's authored rate
via a texture-transform; the port's radial soft-edge mask moved to a second
CLAMPed stage so the WRAPPED scroll rolls flame through the sprite without
scrolling the edge away. Old grit x radial bake kept as the no-BINTA fallback.
Impact hits, damage-band smoke and death booms all ride this layer.
AUTHORED TEXTURE SCROLL in the model path: the BMF TEXTURE records carry
SPECIAL " SCROLL u0 v0 du dv" (tag 0x2037); the draw path always supported
per-op scrolling (SetTextureScrolling) but the BGF loader never parsed it, so
every scrolling material rendered frozen. Wired TexRef -> MatInfo -> batch ->
L4TEXOP.doScroll: the flame cards (flamebig/fire5) now roll fire noise.
VERTEX-ALPHA EFFECT CARDS (the "twisted drill bit of fire" fix): FLAMEBIG's
verts carry authored float RGBA -- white-hot base (1.0,0.99,0.97) -> dark-red
tip fading to alpha -0.2 (the DPL clamp convention). The loader kept a flat
batch colour and drew it OPAQUE = a solid orange spike. Corpus sweep: exactly
14 shipped BGFs carry vertex alpha, ALL effect cards (flames, MUZFLASH,
EXDISK_A/B/C, TMST_A/B/C, beam models, DECLOUDS). Such batches now keep the
authored per-vertex gradient and route to the alpha-blend pass, unlit,
colour = texture x gradient, alpha = the vertex fade; sky objects excluded
(drawAsSky + alphaTest passes NEITHER pass filter -- DECLOUDS stays in the
sky pass). MUZFLASH/EXDISK render correctly for free when the muzzle-model
work lands.
WRECK DRESSING (the 1996 ExplosionScripts case-4 transcription): pieces spawn
HIDDEN and reveal 0.25s after the boom (the InstanceSwitch delay, behind the
dnboom flash); flamebig hangs over the pile, Y-BILLBOARDED at the camera
(SetOffsetYaw + a camera-pos getter -- the dpl_SetDCSReorientAxes analog);
the MakeDCSFall settle arms at the reveal with the two authored rates (hulk/
debris -0.025 t^2, fires -0.01 t^2 -- the flames ride above the sinking pile
and die with it at burial). EMPTY-PLACEHOLDER hulk guard: THRDBR.BGF is a
153-byte zero-geometry stub that "loads fine" -- vertex-count check now routes
it to the gendbr fallback (a Thor wreck was invisible). Hulk content census
recorded: AVADBR==MADDBR==VULDBR geometry (palette-only prefix diffs),
RAPDBR==SNDDBR==STIDBR byte-identical -- wreck variety is materials + the
dressing, not unique piles.
LIVE-PLAY BATCH: muzzle resolve uses the named segmentIndex (raw +0xdc read
was layout garbage); forward launch frame (authored MuzzleVelocity +Z vs the
mech's -Z facing); dock-bottom single window (gauge strip appended below the
world viewport, 1100x600 default, BT_DEV_GAUGES_WINDOW=1 restores the separate
window); portrait sec surface unrotated CW; ammo counters live via typed
bridges (BTAmmoBinCountPtr/BTAmmoBinFeeding/BTWeaponAmmoBin -- raw bin+0x180
and a hand-rolled link walk were garbage); fourth fire key ('4' = Pinky);
panel/arc probes de-aliased (%61 prime).
KB: rendering.md (vertex-alpha card family + scroll), combat-damage.md (hulk
census + THRDBR stub), gauges-hud.md (ammo bridges).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bb795e2805
commit
48c9c8444f
@@ -170,6 +170,21 @@ AmmoBin::~AmmoBin()
|
||||
// to Feeding, reloads the feed timer to feedRate, decrements ammoCount (clamped
|
||||
// at 0) and raises the Empty alarm if that was the last round.
|
||||
//
|
||||
// Panel bridges (Streak-gauge fix, 2026-07-12): the BallisticWeaponCluster
|
||||
// read the bin's rounds via raw `bin+0x180` and the reload state via raw
|
||||
// `bin+0x18c` -- the BINARY's offsets, garbage on our compiled layout (the
|
||||
// HeatWatcher base sizes differ), so the ammo counter showed a constant and
|
||||
// the reload wipe never armed. Complete-type reads of the NAMED members.
|
||||
int *BTAmmoBinCountPtr(void *bin)
|
||||
{
|
||||
return (bin != 0) ? &((AmmoBin *)bin)->ammoCount : 0;
|
||||
}
|
||||
int BTAmmoBinFeeding(void *bin)
|
||||
{
|
||||
// "reloading" for the panel = the bin mid-feed (alarm state Feeding).
|
||||
return (bin != 0 && ((AmmoBin *)bin)->ammoAlarm.Level() == AmmoBin::Feeding) ? 1 : 0;
|
||||
}
|
||||
|
||||
int AmmoBin::FeedAmmo()
|
||||
{
|
||||
if (simulationState == 1) // this+0x40 == 1 (destroyed)
|
||||
|
||||
@@ -78,6 +78,8 @@
|
||||
public HeatWatcher
|
||||
{
|
||||
friend struct AmmoBinLayoutCheck; // compile-time offset locks (ammobin.cpp)
|
||||
friend int *BTAmmoBinCountPtr(void *bin); // panel ammo counter bridge (Streak fix)
|
||||
friend int BTAmmoBinFeeding(void *bin); // panel reload-state bridge
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Shared Data Support
|
||||
//
|
||||
|
||||
@@ -1673,7 +1673,11 @@ void WeaponCluster::Execute()
|
||||
if (getenv("BT_PANEL_LOG"))
|
||||
{
|
||||
static int s_pl = 0;
|
||||
if ((s_pl++ % 60) == 0)
|
||||
if ((s_pl++ % 61) == 0) // 61 PRIME: a shared counter at %60 ALIASES
|
||||
// to one cluster when the panel count divides
|
||||
// 60 (the Avatar's 6 panels -> only AFC100
|
||||
// ever printed; the emitter probe's %97
|
||||
// comment taught this lesson already)
|
||||
DEBUG_STREAM << "[panel] " << (diagWeaponName ? diagWeaponName : "?")
|
||||
<< " percentDone=" << percentDone
|
||||
<< " warn=" << warningState << "\n" << std::flush;
|
||||
@@ -1751,11 +1755,18 @@ BallisticWeaponCluster::BallisticWeaponCluster(
|
||||
reloadSeconds = 0.0f; // @0x3E
|
||||
jammed = firing = reloading = False;
|
||||
|
||||
void *ammoBin = ResolveLink((char *)subsystem_in + 0x43c); // FUN_00417ab4 (raw)
|
||||
// AFC100-counter fix (2026-07-12): the raw ResolveLink walk assumed the
|
||||
// 1995 connection internals -- resolve through the weapon's own typed
|
||||
// connection instead (the same call ConsumeRound fires through).
|
||||
extern void *BTWeaponAmmoBin(void *weapon);
|
||||
void *ammoBin = BTWeaponAmmoBin(subsystem_in);
|
||||
int engPort = renderer_in->FindGraphicsPort(eng_port_name);
|
||||
|
||||
// Ammo count off the bin (+0x180); BEST-EFFORT raw if the bin resolves.
|
||||
int *ammoValue = (ammoBin != NULL) ? (int *)((char *)ammoBin + 0x180) : NULL;
|
||||
// Ammo count via the complete-type bridge (Streak-gauge fix, 2026-07-12):
|
||||
// the old raw `bin+0x180` was the BINARY's ammoCount offset -- garbage on
|
||||
// our compiled AmmoBin (base sizes differ), so the counter never moved.
|
||||
extern int *BTAmmoBinCountPtr(void *bin);
|
||||
int *ammoValue = BTAmmoBinCountPtr(ammoBin);
|
||||
|
||||
ammoCountA = new NumericDisplayInteger(ChildRate(), mfd_mode, renderer_in, // @00470cfc
|
||||
owner_ID, mfd_port, x + 0x27, y + 100, x + 0x5f, y + 0x76, font_a, 4,
|
||||
@@ -1823,7 +1834,8 @@ void BallisticWeaponCluster::Execute()
|
||||
{
|
||||
jammed = (*(int *)((char *)subsystem + 0x364) == 5); // BEST-EFFORT raw (weaponAlarm==Jammed)
|
||||
void *ammoBin = ResolveLink((char *)subsystem + 0x43c); // FUN_00417ab4
|
||||
int reloadState = (ammoBin != NULL) ? *(int *)((char *)ammoBin + 0x18c) : 0;
|
||||
extern int BTAmmoBinFeeding(void *bin);
|
||||
int reloadState = BTAmmoBinFeeding(ammoBin); // was raw bin+0x18c (garbage on our layout)
|
||||
firing = reloadState;
|
||||
if (reloadState == 0)
|
||||
{
|
||||
|
||||
@@ -802,10 +802,16 @@ void
|
||||
//
|
||||
if (render_tree.wreckHulk != NULL) render_tree.wreckHulk->SetDrawObj(NULL);
|
||||
if (render_tree.wreckDebris != NULL) render_tree.wreckDebris->SetDrawObj(NULL);
|
||||
if (render_tree.wreckFlames != NULL) render_tree.wreckFlames->SetDrawObj(NULL);
|
||||
render_tree.wreckHulk = NULL;
|
||||
render_tree.wreckDebris = NULL;
|
||||
render_tree.wreckFlames = NULL;
|
||||
render_tree.wrecked = 0;
|
||||
render_tree.wreckAge = 0.0f;
|
||||
render_tree.wreckRevealed = 0;
|
||||
render_tree.wreckHulkObj = NULL;
|
||||
render_tree.wreckDebrisObj = NULL;
|
||||
render_tree.wreckFlamesObj = NULL;
|
||||
|
||||
//
|
||||
// Restore every body segment to its now-intact mesh via the shared view-skeleton
|
||||
@@ -929,6 +935,16 @@ void
|
||||
}
|
||||
d3d_OBJECT *hulk = (hulk_name[0] != '\0')
|
||||
? d3d_OBJECT::LoadObject(GetDevice(), hulk_name) : NULL;
|
||||
// EMPTY-PLACEHOLDER guard: some shipped hulks are 153-byte stubs with ZERO
|
||||
// geometry (THRDBR.BGF, FLAMESML.BGF) -- they "load" fine and draw nothing,
|
||||
// so the missing-file fallback alone never fires. Treat no-vertex hulks
|
||||
// as missing.
|
||||
if (hulk != NULL && hulk->GetVertCount() == 0)
|
||||
{
|
||||
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
||||
<< "' is an EMPTY placeholder -> gendbr.bgf fallback\n" << std::flush;
|
||||
hulk = NULL;
|
||||
}
|
||||
if (hulk == NULL)
|
||||
{
|
||||
DEBUG_STREAM << "[BTrender] wreck: '" << hulk_name
|
||||
@@ -942,10 +958,25 @@ void
|
||||
//
|
||||
d3d_OBJECT *debris = d3d_OBJECT::LoadObject(GetDevice(), "ldbr.bgf");
|
||||
|
||||
//
|
||||
// The burning-wreck FLAMES (the 1996 case-4 "fires" branch): flamesml +
|
||||
// flamebig hung over the pile; flamebig is Y-BILLBOARDED toward the camera
|
||||
// (dpl_SetDCSReorientAxes) and the fires DCS falls at -0.01 (slower than
|
||||
// the hulk's -0.025, so the flames ride above the sinking pile). In this
|
||||
// content build FLAMESML.BGF is an empty placeholder -- only flamebig
|
||||
// carries geometry (btfx:firesmoke1_mtl, the scrolling fire-noise card,
|
||||
// animated by the authored SCROLL now honoured in the BGF path).
|
||||
//
|
||||
d3d_OBJECT *flames = d3d_OBJECT::LoadObject(GetDevice(), "flamebig.bgf");
|
||||
if (flames != NULL && flames->GetVertCount() == 0)
|
||||
flames = NULL;
|
||||
|
||||
//
|
||||
// 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).
|
||||
// at the mech's ground position with its death yaw). All pieces start
|
||||
// HIDDEN: the 1996 script's InstanceSwitchRenderables reveal them 0.25s
|
||||
// after the boom (behind the dnboom flash) -- TickWreck runs the reveal.
|
||||
//
|
||||
for (std::map<int, HierarchicalDrawComponent*>::iterator r =
|
||||
render_tree.segRenderable.begin();
|
||||
@@ -959,21 +990,39 @@ void
|
||||
dpl_ISECT_MODE isect_mode;
|
||||
LinearMatrix identity(True);
|
||||
if (hulk != NULL)
|
||||
{
|
||||
render_tree.wreckHulk = new DPLStaticChildRenderable(
|
||||
victim, false /* main zone */, hulk,
|
||||
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
||||
render_tree.wreckHulk->SetDrawObj(NULL); // hidden until the reveal
|
||||
}
|
||||
if (debris != NULL)
|
||||
{
|
||||
render_tree.wreckDebris = new DPLStaticChildRenderable(
|
||||
victim, false /* main zone */, debris,
|
||||
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
||||
render_tree.wreckDebris->SetDrawObj(NULL);
|
||||
}
|
||||
if (flames != NULL)
|
||||
{
|
||||
render_tree.wreckFlames = new DPLStaticChildRenderable(
|
||||
victim, false /* main zone */, flames,
|
||||
isect_mode, INTERSECT_ALL, identity, render_tree.rootRenderable);
|
||||
render_tree.wreckFlames->SetDrawObj(NULL);
|
||||
}
|
||||
}
|
||||
render_tree.wrecked = 1;
|
||||
render_tree.wreckAge = 0.0f;
|
||||
render_tree.wrecked = 1;
|
||||
render_tree.wreckAge = 0.0f;
|
||||
render_tree.wreckRevealed = 0;
|
||||
render_tree.wreckHulkObj = hulk;
|
||||
render_tree.wreckDebrisObj = debris;
|
||||
render_tree.wreckFlamesObj = flames;
|
||||
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;
|
||||
<< (flames ? " + flamebig flames" : "")
|
||||
<< " (reveal in 0.25s)\n" << std::flush;
|
||||
}
|
||||
|
||||
|
||||
@@ -1002,14 +1051,40 @@ int
|
||||
return 0; // already buried
|
||||
|
||||
render_tree.wreckAge += dt;
|
||||
float sink = -0.025f * render_tree.wreckAge * render_tree.wreckAge; // the authored rate
|
||||
|
||||
//
|
||||
// The 0.25s REVEAL (the 1996 InstanceSwitch delay): the pieces appear
|
||||
// behind the dnboom flash, then MakeDCSFall arms and the burial starts.
|
||||
//
|
||||
const float kRevealDelay = 0.25f; // static_debris/fires_delay
|
||||
if (!render_tree.wreckRevealed)
|
||||
{
|
||||
if (render_tree.wreckAge < kRevealDelay)
|
||||
return 1;
|
||||
render_tree.wreckRevealed = 1;
|
||||
if (render_tree.wreckHulk)
|
||||
render_tree.wreckHulk->SetDrawObj(render_tree.wreckHulkObj);
|
||||
if (render_tree.wreckDebris)
|
||||
render_tree.wreckDebris->SetDrawObj(render_tree.wreckDebrisObj);
|
||||
if (render_tree.wreckFlames)
|
||||
render_tree.wreckFlames->SetDrawObj(render_tree.wreckFlamesObj);
|
||||
}
|
||||
|
||||
// MakeDCSFall: offsetY = -1/2 g t^2 from the reveal; hulk/debris g=0.025,
|
||||
// fires g=0.01 (the flames ride above the sinking pile).
|
||||
float t = render_tree.wreckAge - kRevealDelay;
|
||||
if (t < 0.0f) t = 0.0f;
|
||||
float sink = -0.025f * t * t; // the authored hulk rate
|
||||
if (sink < -8.0f)
|
||||
{
|
||||
// fully buried -> hide + stop ticking
|
||||
// fully buried -> hide + stop ticking (the flames die with the pile:
|
||||
// the original removed the whole death entity here)
|
||||
if (render_tree.wreckHulk) render_tree.wreckHulk->SetDrawObj(NULL);
|
||||
if (render_tree.wreckDebris) render_tree.wreckDebris->SetDrawObj(NULL);
|
||||
if (render_tree.wreckFlames) render_tree.wreckFlames->SetDrawObj(NULL);
|
||||
render_tree.wreckHulk = NULL;
|
||||
render_tree.wreckDebris = NULL;
|
||||
render_tree.wreckFlames = NULL;
|
||||
DEBUG_STREAM << "[BTrender] wreck buried (sink complete)\n" << std::flush;
|
||||
return 0;
|
||||
}
|
||||
@@ -1017,6 +1092,26 @@ int
|
||||
render_tree.wreckHulk->SetOffsetTranslation(0.0f, sink, 0.0f);
|
||||
if (render_tree.wreckDebris)
|
||||
render_tree.wreckDebris->SetOffsetTranslation(0.0f, sink, 0.0f);
|
||||
if (render_tree.wreckFlames)
|
||||
{
|
||||
render_tree.wreckFlames->SetOffsetTranslation(0.0f, -0.01f * t * t, 0.0f);
|
||||
|
||||
//
|
||||
// Y-BILLBOARD the flame card at the camera (dpl_SetDCSReorientAxes
|
||||
// analog). The parent DCS applies the victim's localToWorld (its
|
||||
// death yaw), so the camera direction is taken in the victim's LOCAL
|
||||
// frame: d_local = R^T * (cam - wreck) using the orthonormal rotation
|
||||
// rows of localToWorld.
|
||||
//
|
||||
float cx, cy, cz;
|
||||
d3d_OBJECT::GetCameraPosition(&cx, &cy, &cz);
|
||||
float dx = cx - (float)victim->localOrigin.linearPosition.x;
|
||||
float dz = cz - (float)victim->localOrigin.linearPosition.z;
|
||||
float lx = dx * (float)victim->localToWorld(0, 0) + dz * (float)victim->localToWorld(0, 2);
|
||||
float lz = dx * (float)victim->localToWorld(2, 0) + dz * (float)victim->localToWorld(2, 2);
|
||||
if (lx * lx + lz * lz > 1e-6f)
|
||||
render_tree.wreckFlames->SetOffsetYaw(atan2f(lx, lz));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -679,7 +679,12 @@ extern void BTDrawReticle(struct IDirect3DDevice9 *device);
|
||||
int wrecked; // 1 = swapped to the <mech>dbr hulk
|
||||
DPLStaticChildRenderable *wreckHulk; // the <mech>dbr piece (sinks)
|
||||
DPLStaticChildRenderable *wreckDebris; // the ldbr scatter (sinks)
|
||||
DPLStaticChildRenderable *wreckFlames; // flamebig card (billboards, sinks slower)
|
||||
float wreckAge; // seconds since the swap
|
||||
int wreckRevealed; // 1 once the 0.25s reveal delay elapsed
|
||||
d3d_OBJECT *wreckHulkObj; // pieces held hidden until the reveal
|
||||
d3d_OBJECT *wreckDebrisObj;
|
||||
d3d_OBJECT *wreckFlamesObj;
|
||||
std::map<int, HierarchicalDrawComponent*> segRenderable; // slot -> joint renderable
|
||||
std::map<int, int> segGState; // slot -> last applied graphic state
|
||||
};
|
||||
|
||||
@@ -644,6 +644,7 @@ int gBTMissileTrigger = 0; // channel 3: missiles (key 3 / CTRL)
|
||||
static int gBTLaserKey = 0; // raw key states (set by the keyboard poll)
|
||||
static int gBTPPCKey = 0;
|
||||
static int gBTMissileKey = 0;
|
||||
static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45)
|
||||
static int gBTConfigKey = 0; // task #6: HOLD 'G' = the weapon-configure button
|
||||
static int gBTGenSelKey = 0; // task #12: F5..F8 = SelectGeneratorA..D, F9 = mode toggle
|
||||
// (0 idle; else the message id 4..8)
|
||||
@@ -786,35 +787,19 @@ void
|
||||
BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos,
|
||||
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided)
|
||||
{
|
||||
// The weapon's GetMuzzlePoint falls back to the mech ORIGIN (feet) when its mount
|
||||
// segment doesn't resolve, so a projectile appeared to launch from the mech's feet.
|
||||
// Resolve the real launch port by NAME off the shooter (same mechanism the visible
|
||||
// laser beams use for the gun ports), alternating the left/right missile ports for a
|
||||
// natural salvo look; fall back to a raised (torso-height) muzzle, then the passed one.
|
||||
// MUZZLE (muzzle wave, 2026-07-12): the passed muzzle is now the weapon's
|
||||
// AUTHENTIC mount segment (GetMuzzlePoint reads the real segmentIndex --
|
||||
// the old raw-offset garbage that made it resolve feet/heads is fixed), so
|
||||
// the port-name rotation hack that compensated for it is RETIRED. Keep
|
||||
// one safety: a degenerate resolve at/below origin height (an unposed
|
||||
// segment) launches from torso height instead of underground.
|
||||
Point3D mz = muzzle;
|
||||
if (shooter != 0)
|
||||
{
|
||||
Mech *sm = (Mech *)shooter;
|
||||
static const char *const kPorts[] =
|
||||
{ "sitermissleport", "sitelmissleport", "siterutorsoport", "sitelutorsoport",
|
||||
"siterugunport", "sitelugunport" };
|
||||
static int s_portRot = 0;
|
||||
EntitySegment *seg = 0;
|
||||
int n = (int)(sizeof(kPorts)/sizeof(kPorts[0]));
|
||||
for (int k = 0; k < n && seg == 0; ++k)
|
||||
if (mz.y - sm->localOrigin.linearPosition.y < 1.0f)
|
||||
{
|
||||
s_portRot = (s_portRot + 1) % n;
|
||||
seg = sm->GetSegment(CString(kPorts[s_portRot]));
|
||||
}
|
||||
if (seg != 0)
|
||||
{
|
||||
AffineMatrix mw;
|
||||
mw.Multiply(seg->GetSegmentToEntity(), sm->localToWorld); // port -> world
|
||||
mz = mw; // W_Axis translation
|
||||
}
|
||||
else
|
||||
{
|
||||
mz = sm->localOrigin.linearPosition; // raise to torso height
|
||||
mz = sm->localOrigin.linearPosition;
|
||||
mz.y += 12.0f;
|
||||
}
|
||||
}
|
||||
@@ -834,8 +819,15 @@ void
|
||||
d.x = tpos.x - mz.x; d.y = tpos.y - mz.y; d.z = tpos.z - mz.z;
|
||||
Scalar len = (Scalar)sqrtf(d.x*d.x + d.y*d.y + d.z*d.z);
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] PUSH target=" << (void*)target << " enemy=" << (void*)gEnemyMech
|
||||
<< " len=" << len << " speed=" << speed << " dmg=" << damage << "\n" << std::flush;
|
||||
{
|
||||
Scalar relY = (shooter != 0)
|
||||
? (mz.y - ((Mech *)shooter)->localOrigin.linearPosition.y) : mz.y;
|
||||
DEBUG_STREAM << "[projectile] PUSH target=" << (void*)target
|
||||
<< " len=" << len << " speed=" << speed << " dmg=" << damage
|
||||
<< " mz=(" << mz.x << "," << mz.y << "," << mz.z << ") relY=" << relY
|
||||
<< " lv=" << (launch_velocity
|
||||
? "(auth)" : "(fallback)") << "\n" << std::flush;
|
||||
}
|
||||
if (len < 0.001f) return;
|
||||
for (int i = 0; i < 64; ++i)
|
||||
{
|
||||
@@ -854,14 +846,21 @@ void
|
||||
int haveLaunch = 0;
|
||||
if (launch_velocity != 0 && shooter != 0)
|
||||
{
|
||||
// FRAME CONVENTION (telemetry-verified 2026-07-12): the authored
|
||||
// MuzzleVelocity is (0,0,+100) -- the launcher frame's +Z is
|
||||
// FORWARD, but the mech's body frame faces -Z. Transforming +Z
|
||||
// through the body basis launched every round BACKWARD (then the
|
||||
// seeker looped it through the ground onto the target --
|
||||
// user-reported). Map launcher-forward (+Z) onto body-forward
|
||||
// (-Z basis): negate the z term.
|
||||
Mech *sm = (Mech *)shooter;
|
||||
UnitVector ax, ay, az;
|
||||
sm->localToWorld.GetFromAxis(X_Axis, &ax);
|
||||
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
|
||||
sm->localToWorld.GetFromAxis(Z_Axis, &az);
|
||||
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y + az.x*launch_velocity->z;
|
||||
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y + az.y*launch_velocity->z;
|
||||
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y + az.z*launch_velocity->z;
|
||||
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y - az.x*launch_velocity->z;
|
||||
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y - az.y*launch_velocity->z;
|
||||
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y - az.z*launch_velocity->z;
|
||||
Scalar lv = (Scalar)sqrtf(p.vel.x*p.vel.x + p.vel.y*p.vel.y + p.vel.z*p.vel.z);
|
||||
if (lv > 1.0f) { p.speed = lv; haveLaunch = 1; }
|
||||
}
|
||||
@@ -882,6 +881,14 @@ void
|
||||
p.damage = damage;
|
||||
p.guided = guided; // autocannon shells fly straight (no seeker in the binary's plain Projectile)
|
||||
p.active = 1;
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] vel=(" << p.vel.x << "," << p.vel.y
|
||||
<< "," << p.vel.z << ")"
|
||||
<< (launch_velocity
|
||||
? " authored=(" : " none=(")
|
||||
<< (launch_velocity ? launch_velocity->x : 0.0f) << ","
|
||||
<< (launch_velocity ? launch_velocity->y : 0.0f) << ","
|
||||
<< (launch_velocity ? launch_velocity->z : 0.0f) << ")\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1693,6 +1700,10 @@ void
|
||||
gBTLaserKey = focused && ((pAsync('1') | pAsync(0x20 /*VK_SPACE*/)) & dn) ? 1 : 0;
|
||||
gBTPPCKey = focused && (pAsync('2') & dn) ? 1 : 0;
|
||||
gBTMissileKey = focused && ((pAsync('3') | pAsync(0x11 /*VK_CONTROL*/)) & dn) ? 1 : 0;
|
||||
// The pod's 4TH fire button (Pinky, 0x45) -- previously
|
||||
// unmapped on desktop, so any weapon the authored groups
|
||||
// put there (the Avatar's NARC etc.) was unreachable.
|
||||
gBTPinkyKey = focused && (pAsync('4') & dn) ? 1 : 0;
|
||||
// task #6: HOLD 'G' opens the config session on the selected
|
||||
// weapon (BT_CONFIG_SLOT, default: the first weapon); while
|
||||
// held, the fire keys TOGGLE its group membership.
|
||||
@@ -1711,7 +1722,7 @@ void
|
||||
gBTValveKey = focused && (pAsync('C') & dn) ? 1 : 0;
|
||||
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
|
||||
// damage dispatcher + the beam-visual keepalive)
|
||||
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey) ? 1 : 0;
|
||||
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey || gBTPinkyKey) ? 1 : 0;
|
||||
static int sPrevX = 0;
|
||||
const int xNow = focused && (pAsync('X') & dn) ? 1 : 0;
|
||||
if (xNow && !sPrevX) gBTDrive.allStop = 1; // edge -> one all-stop
|
||||
@@ -1862,7 +1873,7 @@ void
|
||||
gBTDrive.fire = 0;
|
||||
gBTDrive.fireForced = 0;
|
||||
gBTDrive.forced = 0;
|
||||
gBTLaserKey = gBTPPCKey = gBTMissileKey = 0;
|
||||
gBTLaserKey = gBTPPCKey = gBTMissileKey = gBTPinkyKey = 0;
|
||||
}
|
||||
|
||||
float throttle = gBTDrive.forced ? gBTDrive.forcedThrottle : gBTDrive.throttle;
|
||||
@@ -3251,7 +3262,7 @@ void
|
||||
};
|
||||
int want[4];
|
||||
want[0] = ((gBTLaserKey && targetInArc) || autofire) ? 1 : 0;
|
||||
want[1] = 0; // pinky: unmapped on desktop
|
||||
want[1] = (gBTPinkyKey && targetInArc) ? 1 : 0; // key '4' (was unmapped)
|
||||
want[2] = (gBTPPCKey && targetInArc) ? 1 : 0;
|
||||
want[3] = (gBTMissileKey && targetInArc) ? 1 : 0;
|
||||
if (cm != 0)
|
||||
|
||||
@@ -572,12 +572,16 @@ void
|
||||
void
|
||||
MechWeapon::GetMuzzlePoint(Point3D &point)
|
||||
{
|
||||
// @004b9948 -- resolve the weapon's mount segment (its index is the inherited
|
||||
// subsystem slot at this+0xdc, == binary param_1+0xdc) in the owning Mech's
|
||||
// segment table and transform it to world (FUN_00424da8). Reads live each call
|
||||
// (the phantom cached `muzzlePoint` member is removed -- it collided with the
|
||||
// Emitter subclass's own fields at 0x3F0+).
|
||||
int segIndex = *(int *)((char *)this + 0xdc); // inherited segment index (byte-exact base)
|
||||
// @004b9948 -- resolve the weapon's mount segment in the owning Mech's
|
||||
// segment table and transform it to world (FUN_00424da8).
|
||||
// DATABINDING FIX (muzzle wave, 2026-07-12): the binary's this+0xdc IS the
|
||||
// engine Subsystem's segmentIndex -- but the old raw `*(this+0xdc)` read
|
||||
// OUR compiled layout at that offset (garbage), so every weapon resolved a
|
||||
// RANDOM segment or none: missiles launched from heads/feet/below terrain
|
||||
// (user-hit on the Avatar) and the beam/missile paths grew gun-port
|
||||
// name-lookup hacks to compensate. GetSegmentIndex() is the named member
|
||||
// [T0 SUBSYSTM.h:108] -- authentic mounts on every chassis.
|
||||
int segIndex = GetSegmentIndex();
|
||||
BTResolveWeaponMuzzle(owner, segIndex, point); // mech4.cpp bridge (null-guarded)
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,18 @@ Logical
|
||||
// AmmoBin::FeedAmmo). Returns True when a round was dispensed; otherwise raises
|
||||
// the NoAmmo alarm and returns False. Shared by both FireWeapon spawn paths.
|
||||
//
|
||||
// Panel bridge (AFC100-counter fix, 2026-07-12): resolve the weapon's ammo
|
||||
// bin through the TYPED connection -- the gauge cluster's hand-rolled raw walk
|
||||
// (*(plug+8) twice at subsystem+0x43c) assumed the 1995 connection-object
|
||||
// internals and returned NULL/garbage on our layout, so the panel's ammo
|
||||
// digits never bound (fired shells consumed rounds invisibly).
|
||||
void *
|
||||
BTWeaponAmmoBin(void *weapon)
|
||||
{
|
||||
return (weapon != 0)
|
||||
? (void *)((ProjectileWeapon *)weapon)->ammoBinLink.Resolve() : 0;
|
||||
}
|
||||
|
||||
Logical
|
||||
ProjectileWeapon::ConsumeRound()
|
||||
{
|
||||
|
||||
@@ -320,6 +320,7 @@ class NotationFile;
|
||||
int tracerInterval; // @0x438 resource TracerInterval (+0x1BC)
|
||||
AmmoBinConnection
|
||||
ammoBinLink; // @0x43C embedded 0xC connection to the AmmoBin subsystem
|
||||
friend void *BTWeaponAmmoBin(void *weapon); // panel ammo-bin bridge (AFC100-counter fix)
|
||||
// (ctor @004bcbb0, dtor @004bcbcf, link vtable
|
||||
// @00512424; resolved via the resource AmmoBin
|
||||
// index at +0x1C0) -- LAST field, object ends 0x448
|
||||
|
||||
Reference in New Issue
Block a user