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:
co-authored by
Claude Fable 5
parent
9fff079df4
commit
6f6a39b8c9
@@ -131,7 +131,24 @@ now: closest MECH (`PickRayHit`, damage zones + lock) → closest STRUCTURE (`Wo
|
||||
occludes a mech BEHIND it) → flat ground (`BTGroundRayHit`) → sky (fire-at-nothing). A structure
|
||||
hit designates `mech+0x388 = gBTTerrainEntity` (the no-damage-zone sentinel) + `0x37c` = the entry
|
||||
point, so the range caret reads the structure distance and NO lock ring draws (mech4.cpp:4529, the
|
||||
`des != hotTarget` branch), exactly the reported authentic behavior. ⚠ There is NO ray query
|
||||
`des != hotTarget` branch), exactly the reported authentic behavior.
|
||||
**⚠ SUPERSEDED IN PART (2026-07-19, issue #3): the struck ENTITY, not the sentinel, is now
|
||||
designated.** The binary damages world structures/icons under the boresight [T1]:
|
||||
`MechWeapon::SendDamageMessage` @004b9728 (part_013.c:6765-6794) gates only on "target NOT derived
|
||||
from Mech (Derivation@0x50bdb4) OR aimed zone != -1" — i.e. a NON-MECH target is sent the zone=−1
|
||||
TakeDamageMessage UNCONDITIONALLY — and the Missile contact branch @004be078 dispatches at
|
||||
whatever entity's solid the round struck (owner via the solid's tag pointer) with NO class test.
|
||||
The port: `Mech::WorldStructurePick` (mech.cpp) now returns the struck solid's owning entity
|
||||
(`BoxedSolid::GetOwningSimulation` — every static solid carries its Terrain/CulturalIcon/Door
|
||||
entity, TERRAIN.cpp:107/246, DOOR.cpp:395), the mech4.cpp pick block designates it (sentinel only
|
||||
as fallback), and the projectile contact path grew the non-mech damage-zoned else-branch
|
||||
(@004be078 mirror, direct Dispatch). A plain terrain owner IGNORES the damage
|
||||
(Entity::TakeDamageMessageHandler returns on zone==−1/no zones, ENTITY.cpp:885); a CulturalIcon's
|
||||
own handler maps −1→0 and dies → its FULL res-32 `trkdead` explosion (dtrkboom+dtrkburn) instead
|
||||
of only the ram crunch — see [[rendering]] §Cultural-icon DESTRUCTION. HUD unchanged: the icon is
|
||||
never `hotTarget`, so no hotbox/lock ring (the `des != hotTarget` terrain branch). Verified live:
|
||||
26 laser truck/prop kills (`[cult] TakeDamage type=3` → `DYING res=32`), ram crunch (res 31)
|
||||
unregressed. [T2] ⚠ There is NO ray query
|
||||
against the ground/`containedByNode` `BoundingBoxTree` — `FindBoundingBoxUnder` is DOWNWARD-only
|
||||
(gravity/ground-snap: `*height = FindDistanceBelowBounded`), useless for a horizontal boresight;
|
||||
the static SOLID tree (`BoxedSolidTree::FindBoundingBoxHitBy`) is the only ray-vs-world query.
|
||||
|
||||
+31
-5
@@ -278,11 +278,37 @@ draw states WITHOUT the black-texel keying (which would hole the near-black char
|
||||
Side benefit: tree9 (GRASS tree/leaf cards) + bdet9 (trans-rail lattice) get their authored
|
||||
cutouts too. Post-death shot: irregular char splat, dark brown (71,44,34) lifting to
|
||||
near-terrain tan (115,100,91 vs terrain 131,119,108), no rectangle; pre-death frames
|
||||
unregressed (vehicles stay lit/diffuse — the `hasNormals` gate is untouched). [T2] ⚠ STILL OPEN
|
||||
(#3): (b) the death Explosion (psfx 1008 ddam5) not visually confirmed;
|
||||
(c) the authored BurningState fire visual unreconstructed. For (b): the ram fires the
|
||||
CRUNCH explosion res 31 (small, human-seen); res 32 is the icon's non-crunch explosion —
|
||||
check which the weapon-kill path should fire + psfx scale vs period footage. LESSON: `[cultvis]` log lines verify
|
||||
unregressed (vehicles stay lit/diffuse — the `hasNormals` gate is untouched). [T2]
|
||||
**(b)+(c) CLOSED (2026-07-19, pixel-verified, awaiting human confirm).** The icon death
|
||||
EXPLOSION packages are authored per KILL TYPE [T1, BTL4.RES byte-dump]: the CRUNCH
|
||||
res 31 = `stephit` (ONE video object, effect **1008** = ddam5 damage smoke) — the ram/step
|
||||
squash is SMALL **BY DESIGN**; the full res 32 = `trkdead` = **1015 dtrkboom.pfx** (fiery
|
||||
omni burst: vel 150, rad 3.5 exp 10, orange 2.0/0.7/0.2→1.0/0.3/0, ~1.5s) + **1016
|
||||
dtrkburn.pfx** (the BURNING-WRECK FIRE: 30 rising fire-card particles over a 5s emission,
|
||||
4±1s each ≈ a ~9s fire at the wreck). The whole icon family shares the pattern:
|
||||
bigdead=[1017 dbigboom,1016], meddead=[1018,1016], msldead/twrdead=[1017,1016],
|
||||
smldead=[1020,1022,1021]. **Item (c) verdict: the burning fire is the dtrkburn ONE-SHOT
|
||||
authored INTO the death package** — there is NO looping BurningState fire: the
|
||||
damage-zone effect watcher ctor @0042a984 has exactly ONE call site binary-wide (the
|
||||
Mech ctor, part_012.c:10405), so icon ExplosionTables are inert, and CulturalIcon has no
|
||||
Performance (AlwaysExecute is replication flush). Documented as authentic. **Item (b):
|
||||
the res-32 path is reached by WEAPON kills, which the binary DOES dispatch [T1]:**
|
||||
`MechWeapon::SendDamageMessage` @004b9728 (part_013.c:6765) gates only "target NOT
|
||||
derived from Mech@0x50bdb4 OR aimed zone set" — a non-Mech target under the boresight is
|
||||
damaged UNCONDITIONALLY (zone −1; the icon handler maps −1→0) — and the Missile contact
|
||||
branch @004be078 dispatches a zone=−1 TakeDamageMessage at the struck solid's OWNER with
|
||||
no class test at all. The port never delivered this because the structure pick designated
|
||||
the `gBTTerrainEntity` SENTINEL; now `Mech::WorldStructurePick` returns the struck
|
||||
solid's owning entity (`BoxedSolid::GetOwningSimulation` — every static solid is built
|
||||
with its Terrain/CulturalIcon/Door entity as owner, TERRAIN.cpp:107/246) and mech4.cpp
|
||||
designates IT (mech.cpp `WorldStructurePick`, mech4.cpp pick block + projectile contact
|
||||
else-branch). Truck armor is `WeaponDamagePoints=1` (TRK.DMG) — one laser kills it.
|
||||
Verified live: 26 laser kills `[cult] TakeDamage type=3` → `DYING … res=32` →
|
||||
`DPLIndependantEffect 1015+1016` at the icon origin → BT_SHOT frames show the orange
|
||||
dtrkboom fireball, dtrkburn flames on the fresh wreck, and the rubble aftermath; ram
|
||||
harness re-run still fires res 31 (crunch unregressed). All 24 psfx slots (incl. 15/16)
|
||||
load from the BTDPL.INI pages. Diag: `BT_FIRE_AT_ICON` (designate nearest ahead icon),
|
||||
`BT_FX_TEST="1015,1016"`. LESSON: `[cultvis]` log lines verify
|
||||
the STATE MACHINE, not pixels — only a screen check (BT_SHOT / human) verifies rendering.
|
||||
Diags: `BT_CULT_LOG` (census/damage/death + `[cultvis]` + `[cultobj]` per-object file/type/op
|
||||
flags). MP: replicants transition via `ReadUpdateRecord` → same SetState watcher path (untested
|
||||
|
||||
@@ -258,6 +258,15 @@ CulturalIcon::AttributeIndexSet& CulturalIcon::GetAttributeIndex()
|
||||
// Construction and Destruction
|
||||
//
|
||||
|
||||
//
|
||||
// Live-icon census (issue #3 diag): lets the BT_FIRE_AT_ICON harness designate
|
||||
// a real map icon deterministically (headless weapon-vs-icon verification).
|
||||
// Registration bookkeeping only -- no behavior change; entries are removed in
|
||||
// the dtor (interest streaming can delete icons on zone transitions).
|
||||
//
|
||||
Entity *gBTCultIcons[256];
|
||||
int gBTCultIconCount = 0;
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
CulturalIcon::CulturalIcon(
|
||||
@@ -266,6 +275,10 @@ CulturalIcon::CulturalIcon(
|
||||
) :
|
||||
UnscalableTerrain(creation_message, shared_data)
|
||||
{
|
||||
if (gBTCultIconCount < 256)
|
||||
{
|
||||
gBTCultIcons[gBTCultIconCount++] = this;
|
||||
}
|
||||
|
||||
Str_Copy(teamName, "Defualt", sizeof(teamName));
|
||||
//
|
||||
@@ -428,7 +441,15 @@ CulturalIcon*
|
||||
//
|
||||
CulturalIcon::~CulturalIcon()
|
||||
{
|
||||
|
||||
// live-icon census removal (swap-with-last; see the registration above)
|
||||
for (int ii = 0; ii < gBTCultIconCount; ++ii)
|
||||
{
|
||||
if (gBTCultIcons[ii] == this)
|
||||
{
|
||||
gBTCultIcons[ii] = gBTCultIcons[--gBTCultIconCount];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
//
|
||||
|
||||
@@ -563,7 +563,8 @@ Logical
|
||||
//
|
||||
Logical
|
||||
Mech::WorldStructurePick(const Point3D &start, const Vector3D &dir,
|
||||
Scalar max_range, Point3D *hit_world)
|
||||
Scalar max_range, Point3D *hit_world,
|
||||
Entity **hit_owner)
|
||||
{
|
||||
Scalar dlen = (Scalar)Sqrt(dir.x*dir.x + dir.y*dir.y + dir.z*dir.z);
|
||||
if (dlen < 1e-6f)
|
||||
@@ -574,6 +575,20 @@ Logical
|
||||
return False;
|
||||
if (hit_world != 0)
|
||||
ray.FindEnd(hit_world); // clipped length -> entry point
|
||||
if (hit_owner != 0)
|
||||
{
|
||||
// The struck solid's owning ENTITY (issue #3, item b). Every static solid
|
||||
// carries its owner Simulation in the tag pointer (BoxedSolid ctor;
|
||||
// TERRAIN.cpp:107/246 pass the Terrain/CulturalIcon `this`, DOOR.cpp:395
|
||||
// the Door) and Entity derives from Simulation at offset 0, so this is the
|
||||
// struck map entity. The binary designates/damages the struck ENTITY:
|
||||
// the weapon dispatch @004b9728 (part_013.c:6765) gates ONLY "target not
|
||||
// derived from Mech@0x50bdb4 OR aimed zone set" -- a non-Mech target is
|
||||
// damaged unconditionally -- and the missile contact @004be078 dispatches
|
||||
// a zone=-1 TakeDamageMessage at the struck solid's owner with no class
|
||||
// test at all.
|
||||
*hit_owner = (Entity *)solid->GetOwningSimulation();
|
||||
}
|
||||
return True;
|
||||
}
|
||||
|
||||
|
||||
@@ -699,9 +699,16 @@ public:
|
||||
// tree (garages/walls/props -- the geometry that blocks the walk) along the
|
||||
// boresight and return the entry point of the closest structure hit. The
|
||||
// authentic non-mech world pick (Mover::FindStaticSolidHitBy, the static-world
|
||||
// half of the engine's FindBoxedSolidHitBy). Defined in mech.cpp.
|
||||
// half of the engine's FindBoxedSolidHitBy). hit_owner (optional) receives the
|
||||
// struck solid's OWNING ENTITY (BoxedSolid::GetOwningSimulation -- every static
|
||||
// solid is built with its Terrain/CulturalIcon/Door entity as owner,
|
||||
// TERRAIN.cpp:107/246, DOOR.cpp:395): the binary targets/damages the struck
|
||||
// ENTITY, not a sentinel (weapon dispatch @004b9728 sends to any non-Mech
|
||||
// target; missile contact @004be078 dispatches at the struck solid's owner).
|
||||
// Defined in mech.cpp.
|
||||
Logical WorldStructurePick(const Point3D &start, const Vector3D &dir,
|
||||
Scalar max_range, Point3D *hit_world);
|
||||
Scalar max_range, Point3D *hit_world,
|
||||
Entity **hit_owner = 0);
|
||||
|
||||
// Per-weapon beam render walk (task #33; extracted task #51 so it runs for
|
||||
// EVERY mech -- player, dummy masters, and MP replicants whose emitters
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user