Issue #3 (b)+(c): weapon damage reaches cultural icons -> full trkdead explosion + authored burn fire

(b) The 'tiny explosion' on the ram kill is AUTHORED: crunch res 31 = 'stephit'
(one video object, effect 1008 = ddam5 damage smoke) -- a step/ram squash is
small by design.  The FULL explosion res 32 = 'trkdead' = psfx 15 dtrkboom
(fiery omni burst) + psfx 16 dtrkburn (the burning-wreck fire), reached by
WEAPON kills -- which the 1995 binary dispatches [T1]:
  - MechWeapon::SendDamageMessage @004b9728 (part_013.c:6765) gates only
    "target NOT derived from Mech@0x50bdb4 OR aimed zone set": a non-Mech
    boresight target takes the zone=-1 damage UNCONDITIONALLY;
  - Missile contact @004be078 dispatches at the struck solid's OWNER entity
    with no class test at all.
Port wiring: Mech::WorldStructurePick returns the struck solid's owning entity
(BoxedSolid::GetOwningSimulation; TERRAIN.cpp:107/246 build every static solid
with its Terrain/CulturalIcon/Door entity as owner); the mech4 pick block
designates IT instead of the gBTTerrainEntity sentinel (sentinel = fallback);
the projectile contact path grew the non-mech damage-zoned else-branch
(@004be078 mirror, direct Dispatch).  Plain terrain ignores the damage
(ENTITY.cpp:885 zone==-1 guard); an icon's handler maps -1 -> 0 and dies.
Truck armor is WeaponDamagePoints=1 (TRK.DMG): one laser = one dead truck.

(c) The burning fire is dtrkburn.pfx, authored INTO the death package (every
icon family carries 1016: trkdead/bigdead/meddead/msldead/twrdead).  There is
NO looping BurningState fire in the binary: the damage-zone effect watcher
ctor @0042a984 has exactly one call site (the Mech ctor, part_012.c:10405), so
icon ExplosionTables are inert, and CulturalIcon has no Performance.
Documented as authentic -- nothing invented.

Verified live (BT_SHOT pixel capture): 26 laser kills, [cult] TakeDamage
type=3 -> DYING res=32 -> DPLIndependantEffect 1015+1016 at the icon origin;
frames show the orange dtrkboom fireball, dtrkburn flames on the fresh wreck,
and the rubble aftermath; ram harness re-run still fires crunch res 31.
Diags: BT_FIRE_AT_ICON (designate nearest ahead icon; live-icon census in
CULTURAL.cpp), BT_FX_TEST="1015,1016".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-19 12:02:15 -05:00
co-authored by Claude Fable 5
parent 9fff079df4
commit 6f6a39b8c9
6 changed files with 198 additions and 16 deletions
+102 -6
View File
@@ -1461,6 +1461,32 @@ static void
}
}
}
else if (tgt != 0 && !BTIsRegisteredMech(tgt) && p.damage > 0.0f
&& tgt->damageZoneCount > 0)
{
// NON-MECH victim with damage zones (a CulturalIcon truck/prop
// designated by the world pick -- issue #3, item b). The binary
// missile dispatches a zone=-1 TakeDamageMessage DIRECTLY at
// whatever entity's solid the round struck, with NO class test
// (Missile::MoveAndCollide contact branch -> @004be078: msg id
// 100, zone 0xffffffff, invalid-zone flag 1 -> struck->Dispatch).
// The icon's own handler maps -1 -> 0 and runs the death
// transition (-> the FULL 'trkdead' explosion, res 32).
Damage dmg;
dmg.damageType = Damage::ExplosiveDamageType;
dmg.damageAmount = p.damage;
dmg.burstCount = 1;
dmg.impactPoint = p.pos;
Entity::TakeDamageMessage take_damage(
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
(p.shooter != 0) ? p.shooter->GetEntityID() : EntityID::Null,
-1 /*unaimed; icon handler maps -1 -> zone 0*/, dmg);
tgt->Dispatch(&take_damage);
if (getenv("BT_PROJ_LOG") || getenv("BT_CULT_LOG"))
DEBUG_STREAM << "[projectile] IMPACT (non-mech entity class="
<< (int)tgt->GetClassID() << ") damage=" << p.damage
<< " (@004be078 direct dispatch)\n" << std::flush;
}
p.active = 0;
}
}
@@ -4607,10 +4633,11 @@ void
Point3D structPoint;
float structDist = 1e30f;
bool haveStruct = false;
Entity *structOwner = 0; // the struck solid's owning map entity
if (gBTTerrainEntity != 0)
{
Point3D sp;
if (WorldStructurePick(rayStart, rayDir, 1200.0f, &sp))
if (WorldStructurePick(rayStart, rayDir, 1200.0f, &sp, &structOwner))
{
float dx = sp.x-rs[0], dy = sp.y-rs[1], dz = sp.z-rs[2];
structDist = dx*dx + dy*dy + dz*dz;
@@ -4623,6 +4650,8 @@ void
DEBUG_STREAM << "[wpick] structure hit at ("
<< sp.x << "," << sp.y << "," << sp.z << ") dist="
<< (float)Sqrt(structDist)
<< " owner=" << (void *)structOwner
<< " ownerClass=" << (structOwner ? (int)structOwner->GetClassID() : -1)
<< (hotTarget ? " (mech also under boresight)" : "")
<< "\n" << std::flush;
}
@@ -4637,11 +4666,21 @@ void
}
else if (haveStruct)
{
// a world structure is the closest hit (garage/wall) -- or it
// occludes a mech behind it. Designate it as the non-mech world
// pick (the geometry rides in the POINT; the sentinel entity has
// no damage-zone table -> no lock ring, HudSim part_013.c:5620).
pickTarget = gBTTerrainEntity;
// a world structure is the closest hit (garage/wall/prop) -- or
// it occludes a mech behind it. Designate the struck solid's
// OWNING ENTITY (issue #3, item b): the binary's target slot
// 0x388 holds the struck entity itself, and its weapon damage
// dispatch @004b9728 (part_013.c:6765-6794) sends a zone=-1
// TakeDamageMessage to ANY non-Mech target unconditionally --
// which is what makes CulturalIcons (trucks/props) weapon-
// killable (-> the FULL 'trkdead' explosion, res 32, not just
// the ram 'stephit' crunch). A plain terrain owner ignores it
// (Entity::TakeDamageMessageHandler returns on zone==-1 with no
// zones, ENTITY.cpp:885); an icon's own handler maps -1 -> 0.
// Fall back to the world sentinel only if the solid carried no
// owner. No lock ring either way (hotTarget stays mech-only;
// HudSim part_013.c:5620).
pickTarget = (structOwner != 0) ? structOwner : gBTTerrainEntity;
pickPoint = structPoint;
++gAimGround;
}
@@ -4744,6 +4783,63 @@ void
}
}
// DETERMINISTIC FIRE-AT-ICON TEST (BT_FIRE_AT_ICON, issue #3 item b
// verify): designate the NEAREST live (DefaultState) CulturalIcon as the
// weapon target every frame, bypassing the flaky headless aim ray, so
// BT_AUTOFIRE exercises the production weapon-vs-icon dispatch
// (SendDamageMessage -> icon TakeDamage -> res-32 'trkdead' explosion).
// Diag only, off by default.
if (getenv("BT_FIRE_AT_ICON"))
{
extern Entity *gBTCultIcons[256];
extern int gBTCultIconCount;
// Prefer icons AHEAD (toward the BT_GOTO goal when set) so the kill
// happens in the chase camera's field of view -- pure capture-harness
// convenience, the dispatch path is identical either way.
float fwdX = 0.0f, fwdZ = 0.0f; int haveFwd = 0;
{
const char *gv = getenv("BT_GOTO");
float gx, gz;
if (gv != 0 && sscanf(gv, "%f %f", &gx, &gz) == 2)
{
fwdX = gx - localOrigin.linearPosition.x;
fwdZ = gz - localOrigin.linearPosition.z;
haveFwd = 1;
}
}
Entity *bestIcon = 0; float bestR2 = 1e30f;
for (int ii = 0; ii < gBTCultIconCount; ++ii)
{
CulturalIcon *ic = (CulturalIcon *)gBTCultIcons[ii];
if (ic == 0 || ic->GetSimulationState() != CulturalIcon::DefaultState)
continue; // already dying/burning
Point3D ip = ic->localOrigin.linearPosition;
float dx = ip.x - localOrigin.linearPosition.x;
float dz = ip.z - localOrigin.linearPosition.z;
if (haveFwd && (dx*fwdX + dz*fwdZ) <= 0.0f)
continue; // behind the walk direction -> off-camera
float r2 = dx*dx + dz*dz;
if (r2 < bestR2) { bestR2 = r2; bestIcon = (Entity *)ic; }
}
if (bestIcon != 0)
{
Point3D aimPt = ((CulturalIcon *)bestIcon)->localOrigin.linearPosition;
aimPt.y += 3.0f; // aim into the hull, not the base
MECH_TARGET_ENTITY(this) = bestIcon;
MECH_TARGET_SUBIDX(this) = -1;
MECH_TARGET_POS(this) = aimPt;
static float s_fai = 0.0f; s_fai += dt;
if (s_fai >= 1.0f)
{
s_fai = 0.0f;
DEBUG_STREAM << "[fireicon] designating icon entity="
<< bestIcon->GetEntityID() << " r=" << (float)Sqrt(bestR2)
<< " (" << aimPt.x << "," << aimPt.y << "," << aimPt.z << ")\n"
<< std::flush;
}
}
}
// HUD feeds: the range caret + the hotbox (world point + state) + the
// recovered-Execute instruments (compass, twist tape, group mask).
// The range caret tracks the PICK (authentic: :5639 computes it from