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:
co-authored by
Claude Fable 5
parent
a834866fcd
commit
2036e7da51
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user