diff --git a/game/reconstructed/emitter.hpp b/game/reconstructed/emitter.hpp index e79915b..7c3c6ad 100644 --- a/game/reconstructed/emitter.hpp +++ b/game/reconstructed/emitter.hpp @@ -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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 64646ab..33521d1 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -141,6 +141,9 @@ #include // BoxedSolid / BoxedSolidCollision / BoxedSolidCollisionList #include // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations #include // HostManager::GetEntityPointer (band-effect attacker resolve) +#if !defined(EMITTER_HPP) +# include // 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 diff --git a/game/reconstructed/mechweap.hpp b/game/reconstructed/mechweap.hpp index 342d43f..febc3c2 100644 --- a/game/reconstructed/mechweap.hpp +++ b/game/reconstructed/mechweap.hpp @@ -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);