BT_SELF_DAMAGE: kill your own pilot in solo, so the RESPAWN family is bench-testable at last

Nothing could kill the LOCAL pilot on a bench.  BT_MP_FORCE_DMG only targets
REPLICANTS (mech4.cpp:4884 skips anything else), a solo dummy never shoots back,
and every other damage path needs a second live pod.  So the entire respawn
family -- #70 torso twist after respawn, #22 ammo/weapons not resetting, #55
coolant/heat/generator restore, #57 the respawn latch -- could only ever be
tested by asking a playtester to die.  That is why they are all still open or
awaiting-verification.

BT_SELF_DAMAGE=<amount/sec> dispatches an unaimed TakeDamage at our OWN mech
through the same virtual Entity::Dispatch a real beam hit uses, so the cylinder
hit-location lookup, the zone cascade, the vital-subsystem kill and the whole
authentic death -> 5s -> DropZone -> Mech::Reset cycle all run for real.  No
state is poked.  =60 kills a fresh MadCat in about a minute.

It LATCHES OFF at the first death.  That matters more than it sounds: live
damage knocks the power bus down, which is exactly the signal a respawn test
wants to read.  The first run of this harness produced a torso electrical state
flapping 4 <-> 1 and I nearly filed it as #70 -- it was the harness still
shooting me.  One death, then silence, then measure.

FIRST RESULT -- #70 does NOT reproduce in solo, but something near it does.
After the respawn the MadCat's torso recovers fully: rate back to 0.872665
(the authored 50 deg/s), elec=4 Ready.  Twist is not broken by a respawn.
However the post-respawn mission shows INTERMITTENT power dropouts that a
pristine mission never shows:

    pristine control (no damage, no death):   48/48 samples elec=4,  ZERO dips
    post-respawn (harness silent):            95/97 elec=4,  2 dips to elec=1

A dip zeroes effectiveTwistRate (torso.cpp:570), so a pilot mid-turn feels the
twist cut out -- plausibly what "torso twist stops working after a respawn"
actually is: not a permanent stop but a stutter.  Both dips are transient and
the tail of the run is steady, so it is not a stuck state.  Cause not yet
found; the watched subsystem's own electrical level (wElec) dips with it, while
the generator holds a full 10000V against a 5000V brownout threshold -- so it
is NOT the brownout path in PowerWatcher::UpdateWatch.  Filed here as the next
thread to pull, with the harness that makes it reproducible in one solo run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-07-28 14:02:41 -05:00
co-authored by Claude Opus 5
parent 034e55c7f5
commit c5537a1247
+43
View File
@@ -5541,6 +5541,49 @@ void
gBTGenSelKey = pulse ? s_gsID : 0;
}
// SELF-DAMAGE HARNESS (BT_SELF_DAMAGE=<amount per second>, off by default).
// The whole RESPAWN family -- torso twist after respawn (#70), ammo/weapon
// reset (#22), coolant/heat/generator restore (#55), the respawn latch
// (#57) -- was untestable on a bench because nothing can kill the LOCAL
// pilot in solo: BT_MP_FORCE_DMG only targets REPLICANTS, and a solo
// dummy never shoots back. This dispatches an unaimed TakeDamage at our
// OWN mech once a second, through the SAME virtual Entity::Dispatch path
// a real beam hit uses -- so the cylinder hit-location lookup, the zone
// cascade, the vital-subsystem kill and the authentic death/respawn cycle
// all run for real. Nothing is poked directly.
// e.g. BT_SELF_DAMAGE=40 kills a fresh mech in well under a minute.
// ONE DEATH ONLY: latch off as soon as we die, so everything observed
// AFTER the respawn is the respawn's own doing and not the harness still
// shooting us. (Live damage knocks the power bus down, which is exactly
// the signal a respawn test is trying to read.)
static int s_sdSpent = 0;
if (IsMechDestroyed())
s_sdSpent = 1;
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_SELF_DAMAGE") && !s_sdSpent
&& !IsMechDestroyed() && damageZoneCount > 0)
{
static float s_sdAccum = 0.0f;
s_sdAccum += dt;
if (s_sdAccum >= 1.0f)
{
s_sdAccum = 0.0f;
Damage dmg;
dmg.damageType = Damage::ExplosiveDamageType;
float want = (float)atof(getenv("BT_SELF_DAMAGE"));
dmg.damageAmount = (want > 0.0f) ? want : 20.0f;
dmg.burstCount = 1;
dmg.impactPoint = localOrigin.linearPosition;
Entity::TakeDamageMessage td(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
GetEntityID(), -1 /*unaimed -> cylinder table resolves the zone*/, dmg);
DEBUG_STREAM << "[selfdmg] " << dmg.damageAmount
<< " to own mech (unaimed)" << std::endl << std::flush;
Dispatch(&td);
}
}
// task #13 scripted verify (BT_VALVE_TEST=1): one MoveValve press at
// frame ~600 -- Condenser1's valveState cycles 1 -> 5, so the flow
// redistribution gives it 5/10 of the total coolant flow ([valve] log