Per-weapon beams from the REAL Emitter sim state (authentic per-mech fire)

The visible beams now come from each weapon's own live state instead of the
hardcoded single-look stagger: FireWeapon arms beamFlag + dischargeTimer (the
weapon's authored DischargeTime), ServiceDischarge ends the window, recharge
gates cadence -- so volley-vs-stagger patterns, cadence and colours all emerge
from each mech's real loadout (BLH: 3 lasers + 2 PPCs).  Per weapon: live
muzzle (its own mount; gun-port ordinal fallback when the mount doesn't
resolve -- GetMuzzlePoint returns the feet otherwise), the fire's stored world
endpoint, and the authored PipColor (lasers red 1,0,0 / PPC blue 0,0,1
straight from the .SUB data).  PPC draws as a thicker, brighter bolt.

Filter is by EXACT classID (Emitter 3016 / PPC 3028) -- the derivation check
matched shared recon-stub chains (a Sensor and the MissileLauncher passed and
drew garbage beams from misinterpreted offsets).

The target slot (MECH_TARGET_POS) now carries the TORSO aim point (all
consumers -- beam endpoints, missiles, reticle -- want center-mass, not the
ground between the feet); the projectile fallback lift moved to match.

Accessors: Emitter::BeamOn/BeamEndpoint, MechWeapon::MuzzlePoint/PipColor.
BT_BEAM_LOG=1 samples the live per-weapon beam state.

Verified: [beam] PPC mz=(...,4.49,...) end=(...,7,...) rgb=(0,0,1) -- gun-port
muzzles, torso endpoints, authored colours; kill chain intact (10 hits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-08 17:06:59 -05:00
co-authored by Claude Fable 5
parent a834866fcd
commit 2036e7da51
3 changed files with 120 additions and 79 deletions
+7
View File
@@ -177,6 +177,13 @@ class NotationFile;
void
PrintState(); // slot 13, @004bb014
//
// Beam-render accessors (the visible-beam layer draws each weapon's own
// beam from this live sim state -- the authentic per-weapon fire look).
//
int BeamOn() const { return beamFlag; } // 0x46c
const Point3D &BeamEndpoint() const { return beamEndpoint; } // 0x460
protected:
void
WriteUpdateRecord( // slot 7, @004ba65c
+103 -79
View File
@@ -141,6 +141,9 @@
#include <BOXSOLID.hpp> // BoxedSolid / BoxedSolidCollision / BoxedSolidCollisionList
#include <cultural.hpp> // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations
#include <hostmgr.hpp> // HostManager::GetEntityPointer (band-effect attacker resolve)
#if !defined(EMITTER_HPP)
# include <emitter.hpp> // Emitter/PPC beam state (the per-weapon beam render walk)
#endif
static const Scalar kBehindCull = -1.0e-4f; // _DAT_004ac044
@@ -764,18 +767,15 @@ void
// Bring-up: the mech's own target slot (owner+0x388) isn't populated (the visible
// fire path targets the gEnemyMech global directly), so fall back to it here.
// (The passed targetPos comes from MECH_TARGET_POS, which the targeting block
// now writes at TORSO height -- the fallback lifts to match.)
Point3D tpos = targetPos;
if ((target == 0 || tpos.x == 0.0f) && gEnemyMech != 0)
{
target = gEnemyMech;
tpos = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
}
// AIM at the torso, not the feet: every target-position source here is the
// victim's ORIGIN (ground level between the feet) -- the beams already
// converge at chest height; missiles must too, or they visibly dive at the
// target's feet and the cylinder resolves leg zones for every hit.
if (target != 0)
tpos.y += kMuzzleHeight;
}
Vector3D d;
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);
@@ -2199,80 +2199,97 @@ void
}
}
const int fireWanted = gBTDrive.fireForced || gBTDrive.fire;
if (gBeamCooldown > 0.0f)
gBeamCooldown -= dt;
// Active-discharge tracker. The real Emitter fires ONE beam on the trigger
// edge (FireWeapon @004bace8) then keeps it alive for its DischargeTime via
// ContinueDischarge (@004baa20) called EVERY frame while Firing. FireWeapon
// computes the muzzle LIVE (GetMuzzlePoint @004b9948, the gun segment's current
// world position) and does NOT store it, so the display re-obtains the muzzle
// each frame -> the beam ORIGIN tracks the gun as the mech moves during the
// discharge (you can't run out from under your own beam). We reproduce that:
// start a discharge on the edge, then REDRAW from the live muzzle every frame
// until the beam-on timer runs out. One discharge at a time (beam-on 0.2s <
// stagger 0.37s), matching the staggered chain cadence.
static int s_dischargePort = -1;
static Scalar s_dischargeRemain = 0.0f;
if (fireWanted && gBeamCooldown <= 0.0f)
{
// STAGGERED CHAIN FIRE (decoded cadence): each gun port cycles at
// kPortRecharge (0.2 DischargeTime + 2.0 RechargeRate = 2.2s). A synced
// volley would flash every 2.2s (too slow); the BLH's lasers fire STAGGERED,
// so fire ONE port per stagger tick and rotate -> aggregate = nMuz beams per
// 2.2s (~0.37s between beams for 6 arm ports), a rapid alternating stream
// while each port honours its real 2.2s recharge.
gBeamCooldown = kPortRecharge / (Scalar)nMuz; // stagger interval
static int s_chainIndex = 0;
s_dischargePort = s_chainIndex % nMuz; // begin this port's discharge
s_dischargeRemain = kBeamOnTime; // DischargeTime beam-on
s_chainIndex = (s_chainIndex + 1) % nMuz;
// solo impact (with an enemy, the target block spawns the explosion).
if (!haveEnemy && gExplodeReady == 1)
{
Origin exp_o;
exp_o.linearPosition = aim;
exp_o.angularPosition = localOrigin.angularPosition; // any valid quat
Explosion::MakeMessage m(
Explosion::MakeMessageID, sizeof(Explosion::MakeMessage),
(Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null,
gExplodeRes, Explosion::DefaultFlags, exp_o,
EntityID::Null, GetEntityID());
Explosion *e = Explosion::Make(&m);
if (e) Register_Object(e);
}
static int s_beamLog = 0;
if ((s_beamLog++ % 8) == 0)
DEBUG_STREAM << "[fire] BEAM #" << s_beamLog
<< (haveEnemy ? " -> target" : " -> free")
<< " muzzle=(" << muzzle.x << "," << muzzle.y << "," << muzzle.z
<< ") hit=(" << aim.x << "," << aim.y << "," << aim.z << ")\n" << std::flush;
}
// KEEPALIVE (== ContinueDischarge): while a discharge is live, redraw the beam
// from the LIVE muzzle (muzzles[] is recomputed from the gun segments every
// frame above) to the live aim. ttl = one frame so exactly one beam is alive
// per frame (same brightness as a single persistent beam), re-anchored to the
// moving gun -- the origin stays on the mech for the whole DischargeTime.
if (s_dischargeRemain > 0.0f && s_dischargePort >= 0 && s_dischargePort < nMuz)
// AUTHENTIC PER-WEAPON BEAMS (task #33): each Emitter/PPC draws ITS OWN
// beam from its live sim state. The state machine is the REAL one:
// FireWeapon arms beamFlag + dischargeTimer (= the weapon's authored
// DischargeTime); ServiceDischarge clears it when the window expires;
// the recharge gate is the weapon's own authored RechargeRate. The
// muzzle resolves LIVE from the weapon's real mount segment (the beam
// origin tracks the gun as the mech moves), the endpoint is the fire's
// stored world hit point, and the colour is the weapon's authored
// PipColor. Volley-vs-stagger patterns, cadence and colours all now
// emerge from each mech's real loadout (the BLH mounts 3 lasers + 2
// PPCs) instead of the old hardcoded single-look stagger, so every
// mech type fires like its data says. (Old stagger/keepalive block
// removed; see the git history for the bring-up scaffolding.)
{
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
const Point3D &mz = muzzles[s_dischargePort]; // LIVE muzzle this frame
const Scalar ttl1 = (dt > 1e-4f) ? dt : 1e-4f; // one-frame life (redrawn each frame)
// Steady beam for the discharge (a laser is on, not fading): a WIDE dim RED
// glow carries the ER-laser colour + the scrolling bexp grit, a THIN brighter
// core hot-spots the centre. Kept dim enough that the additive core does not
// wash the glow/grit to white (the run-out fix removed the old fade that used
// to hide the saturation). Widths are per-beam (tube honours b.width now).
BTPushBeam(mz.x, mz.y, mz.z, aim.x, aim.y, aim.z,
0x00C81404u, ttl1, 3.0f); // wide red glow (grit shows)
BTPushBeam(mz.x, mz.y, mz.z, aim.x, aim.y, aim.z,
0x00A07868u, ttl1, 0.9f); // thin warm core (no white-out)
s_dischargeRemain -= dt;
const Scalar ttl1 = (dt > 1e-4f) ? dt : 1e-4f; // one-frame life (redrawn while on)
static int s_beamStateLog = 0;
int energyOrdinal = -1; // Nth energy weapon (port assignment)
for (int wi = 0; wi < GetSubsystemCount(); ++wi)
{
Subsystem *ws = GetSubsystem(wi);
if (ws == 0)
continue;
// EXACT class filter: Emitter (0xBC8=3016) or PPC (0xBD4=3028).
// The derivation check matched too broadly here (the recon
// derivation chains are shared stubs for some subsystems --
// a Sensor and the MissileLauncher passed and drew garbage
// from misinterpreted offsets).
const int wcid = (int)ws->GetClassID();
if (wcid != 3016 && wcid != 3028)
continue;
++energyOrdinal;
Emitter *em = (Emitter *)ws;
if (!em->BeamOn())
continue;
Point3D mz;
em->MuzzlePoint(mz); // LIVE muzzle (tracks the gun)
// MOUNT FALLBACK: when the weapon's mount segment doesn't
// resolve, GetMuzzlePoint returns the mech ORIGIN (feet).
// Assign this weapon its own gun-port segment by roster
// ordinal (the same port set the old visual used) so each
// energy weapon keeps a stable muzzle on the arms.
if (mz.y - localOrigin.linearPosition.y < 1.0f)
{
static const char *const kGunPorts[] =
{ "siterugunport", "sitelugunport", "siterdgunport",
"siteldgunport", "siterbgunport", "sitelbgunport" };
static EntitySegment *s_portCache[64];
static int s_portTried[64];
if (energyOrdinal >= 0 && energyOrdinal < 64)
{
if (!s_portTried[energyOrdinal])
{
s_portTried[energyOrdinal] = 1;
s_portCache[energyOrdinal] = GetSegment(
CString(kGunPorts[energyOrdinal % 6]));
}
if (s_portCache[energyOrdinal] != 0)
{
AffineMatrix mw;
mw.Multiply(s_portCache[energyOrdinal]->GetSegmentToEntity(),
localToWorld);
mz = mw; // Point3D = matrix translation
}
}
}
const Point3D &bend = em->BeamEndpoint(); // the fire's world hit point
// authored per-weapon colour; unset (-1) -> the ER-laser red
RGBColor pc = em->PipColor();
float r = (float)pc.Red, g = (float)pc.Green, b = (float)pc.Blue;
if (r < 0.0f || g < 0.0f || b < 0.0f) { r = 0.78f; g = 0.08f; b = 0.02f; }
// PPC (classID 0xBD4 = 3028): a thicker, brighter bolt than a laser tube.
const int isPPC = ((int)em->GetClassID() == 3028);
unsigned glow =
(((unsigned)(r * 200.0f) & 0xFF) << 16) |
(((unsigned)(g * 200.0f) & 0xFF) << 8) |
((unsigned)(b * 200.0f) & 0xFF);
unsigned core =
(((unsigned)(96.0f + r * 96.0f) & 0xFF) << 16) |
(((unsigned)(96.0f + g * 96.0f) & 0xFF) << 8) |
((unsigned)(96.0f + b * 96.0f) & 0xFF);
BTPushBeam(mz.x, mz.y, mz.z, bend.x, bend.y, bend.z,
glow, ttl1, isPPC ? 4.4f : 3.0f); // wide glow (grit shows)
BTPushBeam(mz.x, mz.y, mz.z, bend.x, bend.y, bend.z,
core, ttl1, isPPC ? 1.5f : 0.9f); // thin bright core
if (getenv("BT_BEAM_LOG") && (s_beamStateLog++ % 30) == 0)
DEBUG_STREAM << "[beam] " << (isPPC ? "PPC" : "laser") << " #" << wi
<< " mz=(" << mz.x << "," << mz.y << "," << mz.z
<< ") end=(" << bend.x << "," << bend.y << "," << bend.z
<< ") rgb=(" << r << "," << g << "," << b << ")\n" << std::flush;
}
}
}
@@ -2289,7 +2306,14 @@ void
{
Point3D enemyPos = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
MECH_TARGET_POS(this) = enemyPos; // mech+0x37c
// The AIM POINT is the target's torso, not its origin (ground level
// between the feet): every consumer of the target slot -- the weapon
// beams' endpoints (GetTargetPosition), the missiles, the reticle --
// wants the center-mass point the pilot is actually aiming at.
Point3D aimPos = enemyPos;
aimPos.y += kMuzzleHeight;
MECH_TARGET_POS(this) = aimPos; // mech+0x37c
MECH_TARGET_ENTITY(this) = gEnemyMech; // mech+0x388 (HasActiveTarget gate)
MECH_TARGET_SUBIDX(this) = -1; // mech+0x38c whole-mech target
+10
View File
@@ -233,6 +233,16 @@ class CockpitHud;
void
GetMuzzlePoint(Point3D &point);
public:
//
// Beam-render accessors (the visible-beam layer draws each weapon's beam
// from its own live state: real muzzle + the authored per-weapon colour).
//
void MuzzlePoint(Point3D &p) { GetMuzzlePoint(p); }
const RGBColor &PipColor() const { return pipColor; }
protected:
// @004b9864 -- build an aim orientation (atan2 of a direction vector).
void
ComputeAimOrientation(LinearMatrix &orientation, const Vector3D &direction);