Impact-FX FORENSICS wave: the i860 specialfx engine, per-round detonations, the ram economy closed

The "we're off the rails" reinvestigation -- a 5-thread evidence workflow
(logs / PFX data / RES model lists / decomp / port audit) + the i860 firmware
decode, then surgical fixes.  Every claim tiered; the wrong turns are on the
record in the KB alongside the corrections.

THE OLD-STYLE SPECIALFX ENGINE [T1, firmware-decoded from VREND.MNG]:
rebuilt scratchpad/i860dis.py (binutils opcode table); mapped the dispatch
(data+0xdd0c; sfx trigger 0xf040cda0, install 0xf040cdc0, step ~0xf0413698,
instance init 0xf04128d8); decoded the heat model EXACTLY: per 30Hz board
frame h *= cool_a; RGB_ch = h_old*(h_new*cook_ch - 0.25) + 0.25 (K=0.25 =
the ember floor @VA 0xF080; kill at h <= 1e-4 @0xF0A0); alpha fades cool_b
x RAW dt (PER-SECOND -- the x30 scaling made laser hits invisible); 7s cap;
y_off = the kill plane; "variance" is DEAD DATA (binary reads "variance",
INI authors "varience").  The 13 descriptors (PPCHit/LaserHit/MissileHit/
Chunks/Sparks/Fireball...) parse from BTDPL.INI and render via the BTPfx
layer -- heat bursts draw the FIERY sheet (brightness over fire), .PFX keeps
GRAYSCALE (authored colours: DNBOOM orange, DDAM gray).

HIT-PACKAGE CENSUS [T1, RES byte-verified]: ppchit=[8] lzrhit=[9] mghit=[7,11]
canhit=[11] mslhit=[10,12,1023] explode=[6]; all 8 mech death lists identical.
Effect routing corrected in BOTH consumers: <100 = specialfx, >=1000 = psfx.

"SILVER MIST x5" ROOT CAUSE: SHKWAVE.PFX (mslhit's 1023) authors maxIssue=5
relPeriod=0.2 rate=1 -- the ONE file where rate contradicts the window; the
emitter trusted rate -> 5 shells at exactly 1Hz.  Emission rate now always
maxIssue/releasePeriod.

PER-ROUND DETONATIONS [T1 @004bef78]: every missile round spawns its own
ExplosionModelFile at ITS impact (hull + terrain) -- a volley ripples 12
fireballs like the demos; rack-tube launch spread [T3] (GUIDED rounds only --
the slot-0 deflection sent every AFC100 shell 3.4deg left/2.5deg down: the
phantom "4th gold beam"); replicant salvos detonate too (launcher index rides
the visual push).  Missile damage bundles through the shooter's messmgr with
the launcher's subsystemID (mslhit fires at the consolidated point; the
binary's dedup CONFIRMED [T1 @0049b784] -- a workflow agent's per-record
claim REFUTED by direct decomp read).

DAMAGE-BAND SEMANTICS [T1]: MechDeathHandler fires the CURRENT band effect on
any damage rise (the binary's changed-flag coalescing) -- a mauled mech under
fire smokes/burns per hit; bands 3/4 are authored fire plumes.

THE RAM ECONOMY -- CLOSED (3 layers, measured live):
 1. armor = POINTS (every zone: damageScale = 1/armorPoints, armor 50-140;
    the [zone-armor] BT_DMG_LOG dump);
 2. StaticBounce prices rams with authored moverMass ~1.3e6 -> ~59,000 pts
    @10m/s; the binary dispatches it RAW but pod MP dropped it on the local
    replicant (MECH.CPP:986 warns) -- ram damage was NETWORK-INERT; our
    task-#47 replicant forwarding surfaced it as a one-shot.  Port
    normalizes x1e-3 to the point economy [T3];
 3. contact EDGE (ramLastVictim/ramContactLinger): the binary's bounce made
    separation implicit; our gait-derived velocity re-priced full rams at
    60Hz (the respawn explode-loop).  One bump = one hit; pressed = BLOCK.

PLUS: sfx size x0.5 + 1.25m visibility floor + hot-phase occlusion [T3];
particle pool 2048->8192 (missile traffic starved laser bursts -- the
"intermittent effects"); replicant beam aging (a lost beam-END record pinned
a stale beam on forever); UV-variant noise stamps (mask-safe mirror/swap);
no scroll on particle stamps (material-path only); BT_FX_TEST/fxshot.py
self-verification harness ([beam-draw]/[zone-armor]/[collide-tx] telemetry).

KB: rendering.md (specialfx engine + census + closeout), combat-damage.md
(ram economy + band semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-13 08:14:12 -05:00
co-authored by Claude Fable 5
parent 48c9c8444f
commit 065c114590
14 changed files with 4404 additions and 82 deletions
+185 -27
View File
@@ -153,6 +153,7 @@
#include <cultural.hpp> // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations
#include <hostmgr.hpp> // HostManager::GetEntityPointer (band-effect attacker resolve)
#include <messmgr.hpp> // SubsystemMessageManager (task #7 consolidated delivery)
#include <mechweap.hpp> // MechWeapon::GetExplosionResourceID (per-round detonation)
#if !defined(PLAYER_HPP)
# include <player.hpp> // Player::VehicleDeadMessage -- the death->respawn notification (task #52)
#endif
@@ -774,18 +775,60 @@ struct BTProjectile {
Scalar aimOffsetY; // vertical aim offset vs the target's origin (live re-lead)
Scalar damage; // <= 0 -> VISUAL-ONLY round (replicant-side salvo mirror)
int guided; // 1 = missile (seeker loft + steering); 0 = ballistic (straight)
Entity *shooter; // the firing mech (messmgr explosion bundling at impact)
int weaponSubsys; // firing weapon's roster index (-1 = unthreaded) --
// resolves the weapon's ExplosionModelFile (mslhit/
// acanhit) in the messmgr, task #7 bundling
int active;
};
static BTProjectile gProjectiles[64];
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
//###########################################################################
// PER-ROUND DETONATION (the binary's Missile::MoveAndCollide @004bef78: every
// round spawns ITS OWN ExplosionModelFile at its impact point, resource
// missile+0x33c -- a volley RIPPLES fireballs across its arrival frames, the
// demo look). Resolved from the firing weapon's roster entry (weapon+0x3E4);
// runs on BOTH nodes (the replicant visual salvo passes its launcher's index
// too). The messmgr's one bundled explosion remains (direct-fire semantics +
// the cross-pod damage stream); among a rippled volley it is invisible.
//###########################################################################
static void
BTSpawnRoundDetonation(Entity *shooter, int weapon_subsys, const Point3D &at)
{
extern int BTIsRegisteredMech(Entity *e);
if (shooter == 0 || weapon_subsys < 0 || !BTIsRegisteredMech(shooter))
return;
Mech *sm = (Mech *)shooter;
if (weapon_subsys >= sm->GetSubsystemCount())
return;
Subsystem *w = sm->GetSubsystem(weapon_subsys);
if (w == 0 || !w->IsDerivedFrom(*MechWeapon::GetClassDerivations()))
return;
ResourceDescription::ResourceID res = ((MechWeapon *)w)->GetExplosionResourceID();
if (res <= 0)
return;
Origin o;
o.linearPosition = at;
o.angularPosition = EulerAngles(Radian(0.0f), Radian(0.0f), Radian(0.0f));
Explosion::MakeMessage m(
Explosion::MakeMessageID, sizeof(Explosion::MakeMessage),
(Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null,
res, Explosion::DefaultFlags, o,
sm->GetEntityID(), sm->GetEntityID());
Explosion *e = Explosion::Make(&m);
if (e)
Register_Object(e);
}
// Called from ProjectileWeapon / MissileLauncher::FireWeapon (via the extern below)
// with the live muzzle, the SHOOTER mech (to resolve the real launch port), the owner's
// locked target entity + point, the launch speed (|muzzleVelocity|), and per-shot damage.
void
BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos,
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided)
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided,
int weapon_subsys)
{
// MUZZLE (muzzle wave, 2026-07-12): the passed muzzle is now the weapon's
// AUTHENTIC mount segment (GetMuzzlePoint reads the real segmentIndex --
@@ -880,6 +923,34 @@ void
? (tpos.y - ((Mech *)target)->localOrigin.linearPosition.y) : 0.0f;
p.damage = damage;
p.guided = guided; // autocannon shells fly straight (no seeker in the binary's plain Projectile)
p.shooter = (Entity *)shooter;
p.weaponSubsys = weapon_subsys;
// RACK-TUBE SPREAD [T3, physically grounded]: the binary's Missile
// entities each launch from their own rack tube (per-tube authored
// MuzzlePosition); our single muzzle superimposed a whole salvo onto
// ONE trajectory -- 12 rounds read as one round, and their 12
// detonations stacked into one frame. Approximate the tube offsets
// with a small deterministic per-slot cone (+-~2.5 deg) + lateral
// muzzle offset; the seeker re-converges them onto the target so the
// volley arrives as the demo's RIPPLE of impacts.
// GUIDED ROUNDS ONLY: a ballistic shell has no seeker to re-converge
// it -- the slot-0 pattern deflected every AFC100 shell a fixed
// 3.4deg left / 2.5deg down (the phantom "4th gold beam" burying its
// tracer beside the target, user-reported). The binary's plain
// Projectile flies straight at the pick.
if (guided)
{
float ja = ((i % 5) - 2) * 0.030f; // yaw +-0.06 rad
float jb = (((i / 5) % 5) - 2) * 0.022f; // pitch +-0.044 rad
float vx = p.vel.x, vy = p.vel.y, vz = p.vel.z;
float ca = cosf(ja), sa = sinf(ja);
p.vel.x = vx * ca - vz * sa;
p.vel.z = vx * sa + vz * ca;
p.vel.y = vy + jb * p.speed;
p.pos.x += ((i % 3) - 1) * 0.6f;
p.pos.z += (((i / 3) % 3) - 1) * 0.6f;
}
p.active = 1;
if (getenv("BT_PROJ_LOG"))
DEBUG_STREAM << "[projectile] vel=(" << p.vel.x << "," << p.vel.y
@@ -888,7 +959,7 @@ void
? " 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;
<< (launch_velocity ? launch_velocity->z : 0.0f) << ")" << std::endl;
return;
}
}
@@ -973,13 +1044,19 @@ static void
if (BTGroundRayHit(prev.x, prev.y, prev.z, rd.x, rd.y, rd.z,
step + 1.0f, &hx, &hy, &hz))
{
// burst on the rock: a tight puff cluster at the hit, no damage
// burst on the rock: the round's own DETONATION (the binary
// missile detonates on ANY geometry, @004bef78) + a tight
// puff cluster, no damage
{
Point3D hp; hp.x = hx; hp.y = hy; hp.z = hz;
BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, hp);
}
extern void BTPfxTrailPuff(int, float, float, float, float, float, float, int);
for (int pf = 0; pf < 4; ++pf)
BTPfxTrailPuff(0, hx, hy, hz, -rd.x, -rd.y, -rd.z, 2);
if (getenv("BT_PROJ_LOG"))
DEBUG_STREAM << "[projectile] WORLD burst at(" << hx << ","
<< hy << "," << hz << ")\n" << std::flush;
<< hy << "," << hz << ")" << std::endl;
p.active = 0;
continue;
}
@@ -1014,6 +1091,7 @@ static void
}
if (contact)
{
BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, p.pos);
Entity *tgt = p.target;
// Deliver to the projectile's target mech -- the launcher set p.target
// from the shooter's 0x388 slot (the picked victim; any peer mech in
@@ -1033,15 +1111,38 @@ static void
dmg.damageAmount = p.damage;
dmg.burstCount = 1;
dmg.impactPoint = p.pos;
Entity::TakeDamageMessage take_damage(
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
0 /*inflictor id: bring-up*/, -1 /*unaimed -> cylinder resolves*/, dmg);
tgt->Dispatch(&take_damage);
// Route through the SHOOTER's SubsystemMessageManager with the
// firing launcher's roster index (task #7 bundling): the
// consolidation resolves roster[id]+0x3E4 = the weapon's
// ExplosionModelFile (mslhit/acanhit) and fires it AT the
// impact point -- the missing missile-hit explosion (the
// laser path always did this via SendDamageMessage; the
// projectile path bypassed the manager entirely).
SubsystemMessageManager *mgr = 0;
if (p.shooter != 0 && p.weaponSubsys >= 0 && BTIsRegisteredMech(p.shooter))
mgr = (SubsystemMessageManager *)((Mech *)p.shooter)->GetMessageManager();
if (mgr != 0)
{
Entity::TakeDamageMessage take_damage(
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
p.shooter->GetEntityID(), -1 /*unaimed -> cylinder resolves*/,
dmg, p.weaponSubsys);
mgr->AddDamageMessage(tgt, &take_damage);
}
else
{
Entity::TakeDamageMessage take_damage(
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
0 /*inflictor id: bring-up*/, -1 /*unaimed -> cylinder resolves*/, dmg);
tgt->Dispatch(&take_damage);
}
// gauge scoring wave (Step 6): a projectile hit credits SCORE too
// (tgt == gEnemyMech here; local player is the viewpoint shooter).
BTPostDamageScore((Entity *)tgt, p.damage);
DEBUG_STREAM << "[projectile] IMPACT damage=" << p.damage
<< " zone=" << take_damage.damageZone << " (cyl-resolved)\n" << std::flush;
<< " subsys=" << p.weaponSubsys
<< (mgr ? " (msgmgr bundled)" : " (direct)")
<< " (zone cyl-resolved)\n" << std::flush;
}
}
p.active = 0;
@@ -1196,6 +1297,8 @@ void
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
localVelocity = Motion::Identity;
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f);
ramLastVictim = 0;
ramContactLinger = 0.0f;
// --- our RELOCATED gait/motion accumulators -> identity (the 1995 offsets
// the decomp zeroes map to these named members in our layout) ---
@@ -1363,7 +1466,7 @@ void
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[death] mech " << GetEntityID()
<< " destroyed -> subsystem shutdown + frozen wreck (IsDisabled="
<< (int)IsDisabled() << ")\n" << std::flush;
<< (int)IsDisabled() << ")" << std::endl;
// --- DEATH EFFECTS (task #42): dispatched HERE, at the VICTIM's own
// once-per-death transition, so they fire regardless of WHAT killed it
@@ -1516,7 +1619,7 @@ void
DEBUG_STREAM << "[mech-exec] entity " << GetEntityID()
<< " instance=" << (int)GetInstance()
<< " pos=(" << localOrigin.linearPosition.x << ", "
<< localOrigin.linearPosition.z << ")\n" << std::flush;
<< localOrigin.linearPosition.z << ")" << std::endl;
}
}
@@ -2023,7 +2126,7 @@ void
<< " err=" << err << " turn=" << gBTGotoTurn
<< " thr=" << gBTGotoThrottle << " arr=" << arrived
<< " fwd=(" << fwdX << "," << fwdZ << ")"
<< " tgt=(" << s_gx << "," << s_gz << ")\n" << std::flush; }
<< " tgt=(" << s_gx << "," << s_gz << ")" << std::endl; }
}
// SELF-DRIVE (task #48): a "beeline" harness must supply its OWN
// forward throttle, not only steering -- otherwise it just turns
@@ -2357,7 +2460,7 @@ void
gCurrentGait = wantGait;
DEBUG_STREAM << "[gait] SequenceController -> blh" << suffix
<< " id=" << (long)*clip << " (" << (wantGait ? "run" : "walk")
<< ")\n" << std::flush;
<< ")" << std::endl;
}
}
adv = bodyAnimation.Advance(dt, 1); // inline clip-select path
@@ -2427,7 +2530,7 @@ void
DEBUG_STREAM << "[gait] adv=" << travelAdv << " (proj=" << adv
<< ") pos=("
<< localOrigin.linearPosition.x << ", "
<< localOrigin.linearPosition.z << ")\n" << std::flush;
<< localOrigin.linearPosition.z << ")" << std::endl;
}
}
else
@@ -2446,7 +2549,7 @@ void
gCurrentGait = wantGait;
gBodyAnimReady = 1;
DEBUG_STREAM << "[anim] gait -> blh" << suffix << " id=" << (long)*clip
<< " (" << (wantGait ? "run" : "walk") << ")\n" << std::flush;
<< " (" << (wantGait ? "run" : "walk") << ")" << std::endl;
}
else
{
@@ -2481,7 +2584,7 @@ void
<< (dir < 0 ? " (reverse)" : "") << " adv=" << adv
<< " loops=" << gBodyAnimLoops
<< " pos=(" << localOrigin.linearPosition.x << ", "
<< localOrigin.linearPosition.z << ")\n" << std::flush;
<< localOrigin.linearPosition.z << ")" << std::endl;
}
}
} // close the BT_GAIT_CUTOVER else (STEP 1-2 path)
@@ -2591,7 +2694,7 @@ void
DEBUG_STREAM << "[collide] pushed out by (" << dx << ", "
<< (localOrigin.linearPosition.y - before.y) << ", " << dz
<< ") pos=(" << localOrigin.linearPosition.x << ", "
<< localOrigin.linearPosition.z << ")\n" << std::flush;
<< localOrigin.linearPosition.z << ")" << std::endl;
}
// Floor clamp: never sink below the base ground (safety on solid-less maps).
if (localOrigin.linearPosition.y < s_groundY)
@@ -2716,7 +2819,7 @@ void
DEBUG_STREAM << "[visgnd] lift=" << vLift
<< " at (" << localOrigin.linearPosition.x << ", "
<< localOrigin.linearPosition.y << ", "
<< localOrigin.linearPosition.z << ")\n" << std::flush;
<< localOrigin.linearPosition.z << ")" << std::endl;
}
}
}
@@ -3804,6 +3907,14 @@ void
// and a multi-solid frame compounds those reflections into a garbage
// velocity that priced mech-vs-mech damage 4x-40x too high (mp_a.log:32651).
frameEntryWorldVelocity = savedWorldVel;
// ram contact-edge decay: once contact with the last victim has been
// broken for the linger window, re-arm the one-bump-one-hit gate
if (ramContactLinger > 0.0f)
{
ramContactLinger -= dt;
if (ramContactLinger <= 0.0f)
ramLastVictim = 0;
}
Damage dmg; // ctor zeroes damageAmount
if (cols != 0)
ProcessCollisionList(cols, dt, old_position, &dmg);
@@ -3831,7 +3942,7 @@ void
}
if (GroundLog())
DEBUG_STREAM << "[ground] CRUNCH (crushable icon) at ("
<< savedPos.x << ", " << savedPos.z << ")\n" << std::flush;
<< savedPos.x << ", " << savedPos.z << ")" << std::endl;
}
else if (dmg.damageAmount > 0.0f) // blocking hit: FULL FRAME REJECTION
{
@@ -3893,12 +4004,12 @@ void
}
if (GroundLog())
DEBUG_STREAM << "[ground] BLOCK + CRASH (iv2=" << iv2
<< " clip=" << animationClips[0x20] << ")\n" << std::flush;
<< " clip=" << animationClips[0x20] << ")" << std::endl;
}
else if (GroundLog())
DEBUG_STREAM << "[ground] BLOCK dmg=" << dmg.damageAmount
<< " at (" << old_position.x << ", " << old_position.z
<< ")\n" << std::flush;
<< ")" << std::endl;
}
// 1-Hz telemetry (verification: h / y / hit-miss); INSIDE-SOLID detector:
@@ -3915,7 +4026,7 @@ void
DEBUG_STREAM << "[ground] INSIDE-SOLID streak at ("
<< localOrigin.linearPosition.x << ", "
<< localOrigin.linearPosition.y << ", "
<< localOrigin.linearPosition.z << ")\n" << std::flush;
<< localOrigin.linearPosition.z << ")" << std::endl;
}
else
s_zeroStreak = 0;
@@ -3928,7 +4039,7 @@ void
<< " lift=" << collisionTemplate->minY
<< " pos=(" << localOrigin.linearPosition.x << ", "
<< localOrigin.linearPosition.y << ", "
<< localOrigin.linearPosition.z << ")\n" << std::flush;
<< localOrigin.linearPosition.z << ")" << std::endl;
}
}
}
@@ -3989,7 +4100,18 @@ static void
{
Damage dmg;
dmg.damageType = Damage::CollisionDamageType;
dmg.damageAmount = resolved->damageAmount;
// RAM ECONOMY NORMALIZATION [T3, evidence-forced 2026-07-12]: StaticBounce
// prices the kinetic loss with the AUTHORED moverMass (~1.3e6 units) --
// ~59,000 "points" for a 10 m/s ram, against zones whose armor absorbs
// 1 point per point (scale = 1/armorPoints, armor 50-140: the [zone-armor]
// dump). The BINARY dispatches this raw number [T1 @part_012:15324] --
// but on the pod network it only ever hit the local REPLICANT of the
// victim, where it evaporated (the master never heard it; MECH.CPP:986
// merely warns). Our MP forwards replicant damage (task #47, required
// for weapons), which surfaced the dormant 59K as a one-shot ram-kill.
// Normalize by 1e-3 (the mass-unit/armor-point scale): a walking bump =
// a few points, a full charge = tens -- the armor-table economy.
dmg.damageAmount = resolved->damageAmount * 0.001f;
dmg.surfaceNormal = resolved->surfaceNormal;
dmg.impactPoint = Point3D(
(collision.collisionSlice.minX + collision.collisionSlice.maxX) * 0.5f,
@@ -4016,9 +4138,11 @@ static void
DEBUG_STREAM << "[collide-tx] " << inflictor->GetEntityID()
<< " rams " << victim->GetEntityID()
<< " dmg=" << resolved->damageAmount
<< " mass=" << inflictor->moverMass
<< " e=" << inflictor->elasticityCoefficient
<< " |v|=" << (Scalar)sqrtf(v.x*v.x + v.y*v.y + v.z*v.z)
<< " at(" << dmg.impactPoint.x << "," << dmg.impactPoint.y
<< "," << dmg.impactPoint.z << ")\n" << std::flush;
<< "," << dmg.impactPoint.z << ")" << std::endl;
}
}
@@ -4100,7 +4224,21 @@ void
// is UNBLOCKED (was deferred: zone==-1 used to be dropped).
if (owner->IsDerivedFrom(*Mech::GetClassDerivations()))
{
BTDispatchCollisionDamage(this, owner, damage, collision);
// RAM CONTACT EDGE (see mech.hpp): dispatch damage only on a FRESH
// contact with this victim; while pressed, refresh the linger so
// sustained contact (held stick / respawn overlap) stays a BLOCK,
// not a 60Hz damage grinder -- the binary's bounce-separation made
// this edge implicit; our gait-derived velocity needs it explicit.
if (owner == ramLastVictim && ramContactLinger > 0.0f)
{
ramContactLinger = 0.35f; // still pressed: refresh, no damage
}
else
{
ramLastVictim = owner;
ramContactLinger = 0.35f;
BTDispatchCollisionDamage(this, owner, damage, collision);
}
}
}
@@ -4234,6 +4372,11 @@ void
continue;
++energyOrdinal;
Emitter *em = (Emitter *)ws;
// replicant beams: age the replicated discharge locally (a lost
// beam-END record otherwise pins the beam on forever -- see
// Emitter::ReplicantServiceBeam)
if (GetInstance() == Entity::ReplicantInstance)
em->ReplicantServiceBeam(ttl1);
if (!em->BeamOn())
continue;
Point3D mz;
@@ -4293,6 +4436,21 @@ void
(((unsigned)(40.0f + r * 215.0f) & 0xFF) << 16) |
(((unsigned)(40.0f + g * 215.0f) & 0xFF) << 8) |
((unsigned)(40.0f + b * 215.0f) & 0xFF);
// PHANTOM-BEAM FORENSICS (BT_FIRE_LOG): identify every drawn beam --
// owner/instance, roster slot, class, discharge state, colour, ends.
if (getenv("BT_FIRE_LOG"))
{
static int s_bd = 0;
if ((++s_bd % 30) == 1) // ~2Hz per sustained beam
DEBUG_STREAM << "[beam-draw] mech=" << GetEntityID()
<< " inst=" << (int)GetInstance()
<< " slot=" << wi << " cid=" << wcid
<< " timer=" << em->DischargeTimer()
<< " pip=(" << r << "," << g << "," << b << ")"
<< " mz=(" << mz.x << "," << mz.y << "," << mz.z << ")"
<< " end=(" << bend.x << "," << bend.y << "," << bend.z
<< ")" << std::endl;
}
BTPushBeamKind(mz.x, mz.y, mz.z, bend.x, bend.y, bend.z,
tint, ttl1, 1.0f /* natural model width */,
isPPC ? 1 : 0 /* ppc.bgf : ermlaser.bgf */);
@@ -4300,6 +4458,6 @@ void
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;
<< ") rgb=(" << r << "," << g << "," << b << ")" << std::endl;
}
}