Combat visible + killable: Wword root-cause fix, .PFX effect layer, RemakeEntity swap
The 'can't kill the enemy / no visible damage' cluster, root-caused and fixed faithfully: - STEP-6 unaimed path was INERT: the cylinder table was 'cached' at Wword(0x111) -- the recon ABSORBER bank (stores nothing, reads 0) -- so every unaimed hit silently no-op'd. Promoted to the named member Mech::damageLookupTable (binary this[0x111], was mislabeled ammoExpended). New gotcha class recorded (reconstruction-gotchas §2) + sweep; 2 dead multiplayer branches logged. - Fire path migrated off the stale vital-zone aim onto the completed STEP-6 unaimed dispatch (zone=-1 + beam entry point -> cylinder resolves the exterior zone). No more invisible 1-shot kills; death via the authentic cascade (~14 center-mass hits). Wreck stays TARGETED on kill (beams stop on it); scoring latches off. - SendSubsystemDamage AV fixed: unbound critical-subsystem plug guard (43 unbound plugs/mech logged as an open question -- the binding itself is a gap). - RemakeEntity (render damage swap): the 1996 render state machine's missing Remake state, reconstructed as an in-place SetDrawObj mesh swap keyed by each segment's damage-zone graphic state (tree dtor doesn't cascade -> never rebuild). Destroyed arms/guns visibly wreck (the only variants the RES registers). - BT .PFX particle layer (L4VIDEO.cpp): the 1995 explosion/damage effect layer, unported since 2007 (DPLIndependantEffect/ReadPSFX/ExplosionScripts all stubs). Parses the authentic VIDEO/*.PFX definitions via the [pfx_day] psfxN mapping; premultiplied blending renders BOTH families from the same data (additive-style fire + occluding smoke -- DDAM2 is 30% grey, DDTHSMK ramps negative: impossible additively); depth-sorted billboards with a radial-masked grit sprite; impact-frame orientation (.PFX offsets are authored mech-local, -Z = out of the struck armor toward the shooter) for weapon hits AND damage bands (via lastInflictingID, now maintained -- was declared but never written). Both effect-number encodings route (raw dpl <100 + WinTesla 1000+slot carried by the band resources). Death fires the authentic dnboom (7) + ddthsmk smoke plume (1). - Effects anchor at the impact point / damaged zone's segment, not the mech origin (no more fire at the feet). - Dev force-input gates BT_AUTOFIRE / BT_AUTODRIVE for headless fire-chain verification; BT_PFX_ADD=1 flips the particle blend for A/B. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7c455303bd
commit
a3d67cc639
+152
-27
@@ -140,6 +140,7 @@
|
||||
#include <BOXTREE.hpp> // BoundingBoxTreeNode::FindBoundingBoxUnder / ...ContainingColumn
|
||||
#include <BOXSOLID.hpp> // BoxedSolid / BoxedSolidCollision / BoxedSolidCollisionList
|
||||
#include <cultural.hpp> // CulturalIcon::IsStoppingCollisionVolume / GetClassDerivations
|
||||
#include <hostmgr.hpp> // HostManager::GetEntityPointer (band-effect attacker resolve)
|
||||
|
||||
static const Scalar kBehindCull = -1.0e-4f; // _DAT_004ac044
|
||||
|
||||
@@ -822,7 +823,10 @@ static void
|
||||
Mech *m = (Mech *)tgt;
|
||||
if (m->damageZoneCount > 0)
|
||||
{
|
||||
int zone = m->FirstVitalZone(); // concentrated fire -> a kill
|
||||
// UNAIMED (STEP 6): the projectile's world impact position IS the
|
||||
// hit point; Mech::TakeDamageMessageHandler resolves the struck
|
||||
// zone from the cylinder hit-location table. (Previously this
|
||||
// aimed the internal vital zone directly -> invisible insta-kill.)
|
||||
Damage dmg;
|
||||
dmg.damageType = Damage::ExplosiveDamageType;
|
||||
dmg.damageAmount = p.damage;
|
||||
@@ -830,12 +834,13 @@ static void
|
||||
dmg.impactPoint = p.pos;
|
||||
Entity::TakeDamageMessage take_damage(
|
||||
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
|
||||
0 /*inflictor id: bring-up*/, zone, dmg);
|
||||
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=" << zone << "\n" << std::flush;
|
||||
DEBUG_STREAM << "[projectile] IMPACT damage=" << p.damage
|
||||
<< " zone=" << take_damage.damageZone << " (cyl-resolved)\n" << std::flush;
|
||||
}
|
||||
}
|
||||
p.active = 0;
|
||||
@@ -853,7 +858,7 @@ static void
|
||||
// derives the position from the subsystem (mech+0x184); we use the mech origin.
|
||||
//###########################################################################
|
||||
void
|
||||
BTSpawnDamageEffect(Mech *mech, int effect_resource)
|
||||
BTSpawnDamageEffect(Mech *mech, int effect_resource, int segment_index)
|
||||
{
|
||||
if (mech == 0)
|
||||
return;
|
||||
@@ -862,7 +867,55 @@ void
|
||||
res = gExplodeRes; // fall back to the resolved generic explosion
|
||||
if (res <= 0)
|
||||
return; // nothing to spawn yet
|
||||
Origin o = mech->localOrigin; // at the mech (binary: per-subsystem)
|
||||
|
||||
//
|
||||
// Effect position: the damaged zone's SEGMENT, in world space (the binary
|
||||
// derives the effect position from the damaged subsystem/zone, not the mech
|
||||
// origin -- an origin-anchored effect burns at ground level between the
|
||||
// feet). Resolve segment_index through the segment table exactly as the
|
||||
// gun-port muzzles do (GetSegmentToEntity x localToWorld); fall back to
|
||||
// torso height over the origin when the zone has no segment.
|
||||
//
|
||||
Origin o = mech->localOrigin;
|
||||
Point3D fxPos = o.linearPosition;
|
||||
fxPos.y += kMuzzleHeight; // default: torso height
|
||||
if (segment_index >= 0)
|
||||
{
|
||||
EntitySegment::SegmentTableIterator it(mech->segmentTable);
|
||||
EntitySegment *seg;
|
||||
while ((seg = it.ReadAndNext()) != NULL)
|
||||
{
|
||||
if (seg->GetIndex() == segment_index)
|
||||
{
|
||||
AffineMatrix mw;
|
||||
mw.Multiply(seg->GetSegmentToEntity(), mech->localToWorld);
|
||||
fxPos = mw; // Point3D = matrix translation
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
o.linearPosition = fxPos;
|
||||
|
||||
// IMPACT FRAME for the band effect: orient local -Z from the victim toward
|
||||
// the LAST ATTACKER (lastInflictingID, maintained by TakeDamageMessageHandler)
|
||||
// -- the .PFX offsets are authored "out of the struck armor". Falls back to
|
||||
// the victim's own frame when the attacker can't be resolved.
|
||||
if (application != 0 && application->GetHostManager() != 0)
|
||||
{
|
||||
Entity *attacker =
|
||||
application->GetHostManager()->GetEntityPointer(mech->lastInflictingID);
|
||||
if (attacker != 0 && attacker != (Entity *)mech)
|
||||
{
|
||||
float adx = (float)(mech->localOrigin.linearPosition.x
|
||||
- attacker->localOrigin.linearPosition.x);
|
||||
float adz = (float)(mech->localOrigin.linearPosition.z
|
||||
- attacker->localOrigin.linearPosition.z);
|
||||
if (adx * adx + adz * adz > 1e-6f)
|
||||
o.angularPosition = // -Z at the attacker
|
||||
EulerAngles(0.0f, (Scalar)atan2((double)adx, (double)adz), 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
Explosion::MakeMessage m(
|
||||
Explosion::MakeMessageID, sizeof(Explosion::MakeMessage),
|
||||
(Entity::ClassID)RegisteredClass::ExplosionClassID, EntityID::Null,
|
||||
@@ -1088,6 +1141,27 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
// DEV: BT_AUTOFIRE=1 holds the trigger (drives the fireForced hook) and
|
||||
// BT_AUTODRIVE=<0..1> holds the throttle (drives the forced hook) so the
|
||||
// full walk->fire->damage->death chain can be exercised headlessly.
|
||||
{
|
||||
static int sAutoFire = -1;
|
||||
static float sAutoDrive = -1.0f;
|
||||
if (sAutoFire < 0)
|
||||
{
|
||||
const char *af = getenv("BT_AUTOFIRE");
|
||||
sAutoFire = (af && *af == '1') ? 1 : 0;
|
||||
const char *ad = getenv("BT_AUTODRIVE");
|
||||
sAutoDrive = ad ? (float)atof(ad) : 0.0f;
|
||||
}
|
||||
gBTDrive.fireForced = sAutoFire;
|
||||
if (sAutoDrive > 0.0f)
|
||||
{
|
||||
gBTDrive.forced = 1;
|
||||
gBTDrive.forcedThrottle = sAutoDrive;
|
||||
}
|
||||
}
|
||||
|
||||
if (gBTDrive.allStop) { sLever = 0.0f; sDetent = 0; gBTDrive.allStop = 0; }
|
||||
|
||||
if (getenv("BT_KEY_LOG"))
|
||||
@@ -2216,7 +2290,32 @@ void
|
||||
gFireCooldown = kFireCooldown;
|
||||
++gShotCount;
|
||||
|
||||
Origin exp_origin = ((Mech *)gEnemyMech)->localOrigin; // at the target
|
||||
// Beam entry point: on the enemy's surface toward the shooter, at
|
||||
// the beam's convergence height. (Exactly the axis point would be
|
||||
// angularly degenerate for the cylinder sector lookup.) Used for
|
||||
// BOTH the hit-explosion position and the unaimed damage dispatch --
|
||||
// the effect fires where the beam lands (chest height, facing side),
|
||||
// not at the mech origin (ground level between the feet).
|
||||
Point3D impact = enemyPos;
|
||||
if (range > 1e-3f)
|
||||
{
|
||||
impact.x -= (ddx / range) * 3.0f; // ~torso radius toward shooter
|
||||
impact.z -= (ddz / range) * 3.0f;
|
||||
}
|
||||
impact.y += kMuzzleHeight; // chest height (beam aim height)
|
||||
|
||||
Origin exp_origin = ((Mech *)gEnemyMech)->localOrigin;
|
||||
exp_origin.linearPosition = impact; // at the hit point
|
||||
// IMPACT FRAME: the .PFX hit effects are authored with local -Z =
|
||||
// "out of the struck armor" (DAFC offsets/velocities spray -Z).
|
||||
// That is the IMPACT normal -- toward the SHOOTER -- not the
|
||||
// victim's body front: with the victim's own quat a rear/side hit
|
||||
// flashed on the FAR (front) side of the mech. Build the frame as
|
||||
// a yaw with -Z aimed from the victim at the shooter. Engine yaw
|
||||
// convention (MATRIX.cpp:196-209): forward -Z = (-sin y, 0, -cos y)
|
||||
// -> yaw = atan2(ddx, ddz) points -Z at the shooter. [T0]
|
||||
exp_origin.angularPosition =
|
||||
EulerAngles(0.0f, (Scalar)atan2((double)ddx, (double)ddz), 0.0f);
|
||||
|
||||
Explosion::MakeMessage exp_message(
|
||||
Explosion::MakeMessageID,
|
||||
@@ -2241,21 +2340,19 @@ void
|
||||
DEBUG_STREAM << "[fire] Explosion::Make returned NULL\n" << std::flush;
|
||||
}
|
||||
|
||||
// --- DAMAGE (real): dispatch a TakeDamage message to a VALID zone; the
|
||||
// engine base handler routes it to Mech__DamageZone::TakeDamage (the real
|
||||
// armor/structure model). Aim a rotating zone so the whole mech degrades;
|
||||
// read back structureLevel (now valid -- friend access) to show it climb
|
||||
// toward 1.0 (destroyed).
|
||||
// --- DAMAGE (real, STEP 6): dispatch UNAIMED (zone == -1) with the
|
||||
// beam's world entry point; Mech::TakeDamageMessageHandler resolves
|
||||
// the zone from the cylinder hit-location table (the STEP-6
|
||||
// reconstruction) and the base handler routes it to
|
||||
// Mech__DamageZone::TakeDamage (the real armor/structure model).
|
||||
// Hits therefore land on the EXTERIOR zone facing the shooter
|
||||
// (arm/leg/torso -- zones with visible segments that wreck), and
|
||||
// internal vitals die only through the authentic destruction
|
||||
// cascade (RecurseSegmentTable / SendSubsystemDamage) -- no more
|
||||
// invisible one-shot kills on the soft internal vital zone.
|
||||
if (gEnemyMech->damageZoneCount > 0)
|
||||
{
|
||||
int zc = gEnemyMech->damageZoneCount;
|
||||
// Aim the first VITAL zone so concentrated fire destroys it (-> mech
|
||||
// death). The faithful per-impact aim (cylinder lookup from the hit
|
||||
// point) is STEP 6; until then we target a vital zone directly.
|
||||
int zone = 0;
|
||||
for (int k = 0; k < zc; ++k)
|
||||
if (((Mech *)gEnemyMech)->Zone(k)->vitalDamageZone) { zone = k; break; }
|
||||
|
||||
// (impact computed above -- shared with the hit-explosion origin)
|
||||
Damage dmg; // default-constructed
|
||||
// Explosive: the weapon effect is an Explosion (explosive), not an
|
||||
// energy beam. Also the correct type to exercise the zone armour/
|
||||
@@ -2265,23 +2362,32 @@ void
|
||||
dmg.damageType = Damage::ExplosiveDamageType;
|
||||
dmg.damageAmount = kShotDamage;
|
||||
dmg.burstCount = 1;
|
||||
dmg.impactPoint = enemyPos; // world impact point
|
||||
dmg.impactPoint = impact; // world impact point
|
||||
|
||||
Entity::TakeDamageMessage take_damage(
|
||||
Entity::TakeDamageMessageID,
|
||||
sizeof(Entity::TakeDamageMessage),
|
||||
GetEntityID(), // inflicting = this (shooter)
|
||||
zone, // valid zone -> base handler applies
|
||||
-1, // UNAIMED -> receiver's cylinder resolves
|
||||
dmg);
|
||||
gEnemyMech->Dispatch(&take_damage);
|
||||
|
||||
// gauge scoring wave (Step 6): credit the local player for damage
|
||||
// dealt -> SCORE climbs per hit (currentScore += tonnageRatio*award).
|
||||
BTPostDamageScore(gEnemyMech, kShotDamage);
|
||||
// No score for pounding the wreck (damage still applies -- zones
|
||||
// clamp at 1.0, further segments wreck visibly).
|
||||
if (!gEnemyDestroyed)
|
||||
BTPostDamageScore(gEnemyMech, kShotDamage);
|
||||
|
||||
Scalar s = ((Mech *)gEnemyMech)->Zone(zone)->damageLevel; // [0,1], 1.0=destroyed (engine base field)
|
||||
DEBUG_STREAM << "[damage] hit zone " << zone << "/" << zc
|
||||
<< " structure=" << s << "\n" << std::flush;
|
||||
// The handler wrote the resolved zone back into the message.
|
||||
int zone = take_damage.damageZone;
|
||||
if (zone >= 0 && zone < gEnemyMech->damageZoneCount)
|
||||
{
|
||||
Scalar s = ((Mech *)gEnemyMech)->Zone(zone)->damageLevel; // [0,1], 1.0=destroyed
|
||||
DEBUG_STREAM << "[damage] hit zone " << zone << "/"
|
||||
<< gEnemyMech->damageZoneCount
|
||||
<< " structure=" << s << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
// Death via the REAL damage model: a mech with a destroyed VITAL zone is
|
||||
@@ -2304,8 +2410,22 @@ void
|
||||
// fires; the ownerless dummy yields no death, DEATHS stays 0).
|
||||
BTPostKillScore(gEnemyMech, kShotDamage);
|
||||
|
||||
// Death explosion at the target.
|
||||
// Death effects, per the authentic BTDPL.INI effect-number map:
|
||||
// 7 = "the big explosion used as part of mech death" (dnboom)
|
||||
// 1 = "the mech death/rubble smoke plume" (ddthsmk)
|
||||
// Fired directly into the render effect layer at the wreck (the
|
||||
// unexported death sequence's effect chain dispatched these
|
||||
// numbers through the 0xBD3 manager; the numbers are the data).
|
||||
{
|
||||
extern void BTStartPfx(int effect_number, float x, float y, float z);
|
||||
Point3D wreck = ((Mech *)gEnemyMech)->localOrigin.linearPosition;
|
||||
BTStartPfx(7, wreck.x, wreck.y + kMuzzleHeight, wreck.z); // the death boom
|
||||
BTStartPfx(1, wreck.x, wreck.y + kMuzzleHeight, wreck.z); // the smoke plume
|
||||
}
|
||||
|
||||
// Death explosion at the target (torso height, not ground level).
|
||||
Origin death_origin = ((Mech *)gEnemyMech)->localOrigin;
|
||||
death_origin.linearPosition.y += kMuzzleHeight;
|
||||
Explosion::MakeMessage death_exp(
|
||||
Explosion::MakeMessageID,
|
||||
sizeof(Explosion::MakeMessage),
|
||||
@@ -2358,7 +2478,12 @@ void
|
||||
DEBUG_STREAM << "[damage] *** TARGET DESTROYED after "
|
||||
<< gShotCount << " hits ***\n" << std::flush;
|
||||
|
||||
gEnemyMech = 0; // stop targeting/firing the dead entity
|
||||
// KEEP the wreck targeted (do NOT null gEnemyMech): the wreck
|
||||
// STAYS in the world (removal = the P5 teardown crash), so the
|
||||
// beam convergence must keep terminating ON it -- nulling the
|
||||
// lock here made every later beam a "free" ray that visibly
|
||||
// passed through the standing wreck. Damage + kill scoring are
|
||||
// latched off above via gEnemyDestroyed.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user