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
@@ -294,6 +294,19 @@ HierarchicalDrawComponent*
|
||||
entity, VideoRenderable::Dynamic, NULL,
|
||||
inDeathZone, intersect_mode, intersect_mask);
|
||||
|
||||
//
|
||||
// Start (or reset) this mech's RemakeEntity bookkeeping: record the skeleton
|
||||
// variant now; the per-segment renderables + initial graphic states are filled
|
||||
// in as the tree is built below (see RemakeEntityRenderables).
|
||||
//
|
||||
MechRenderTree &render_tree = mMechRenderTrees[entity];
|
||||
render_tree = MechRenderTree();
|
||||
render_tree.skeletonType = (int)skeletonType;
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[BTrender] tracking mech tree for entity " << (void*)entity
|
||||
<< " classID=" << entity->GetClassID() << " ("
|
||||
<< mMechRenderTrees.size() << " tracked)\n" << std::flush;
|
||||
|
||||
//
|
||||
// Per-segment renderable array (the parent for each segment's children).
|
||||
//
|
||||
@@ -368,7 +381,20 @@ HierarchicalDrawComponent*
|
||||
// Load this segment's geometry (skeleton-variant .bgf), if any.
|
||||
//
|
||||
d3d_OBJECT *this_object = NULL;
|
||||
CString *object_name = segment->GetVideoObjectName(skeletonType); // FUN_00424084
|
||||
// Select the segment's model VARIANT by its damage zone's graphic state.
|
||||
// The engine keys video-object names by {skeleton, damage_graphic_state}
|
||||
// (SEGMENT.h:172): a Destroyed zone (GetGraphicState()==1) returns the
|
||||
// destroyed/damaged model, so a wrecked segment visibly comes apart. The
|
||||
// recon previously passed ONLY the skeleton type, leaving the state at its
|
||||
// default 0 (Exists) -> always the intact model = no visible damage.
|
||||
Enumeration seg_gstate = 0; // ExistsGraphicState
|
||||
{
|
||||
int zone_index = segment->GetPrimaryDamageZone(); // SEGMENT.h:107 (a zone INDEX)
|
||||
if (zone_index >= 0 && zone_index < entity->damageZoneCount
|
||||
&& entity->damageZones[zone_index] != 0)
|
||||
seg_gstate = entity->damageZones[zone_index]->GetGraphicState(); // DAMAGE.h:196
|
||||
}
|
||||
CString *object_name = segment->GetVideoObjectName(skeletonType, seg_gstate); // FUN_00424084
|
||||
if (object_name != NULL)
|
||||
{
|
||||
char filename[44];
|
||||
@@ -454,6 +480,11 @@ HierarchicalDrawComponent*
|
||||
}
|
||||
}
|
||||
dcs_array[segment_slot] = child;
|
||||
|
||||
// Record this segment's renderable + the graphic state it was built with,
|
||||
// so a later damage-state change can swap its mesh in place (RemakeEntity).
|
||||
render_tree.segRenderable[segment_slot] = child;
|
||||
render_tree.segGState[segment_slot] = (int)seg_gstate;
|
||||
}
|
||||
|
||||
delete [] dcs_array;
|
||||
@@ -539,6 +570,140 @@ HierarchicalDrawComponent*
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// RemakeEntityRenderables (the render "RemakeEntity" state -- damage swap)
|
||||
//#############################################################################
|
||||
//
|
||||
// A damage zone's graphic state changed (a segment became Destroyed or Gone).
|
||||
// Walk this mech's segments and, for any whose graphic state now differs from
|
||||
// what its renderable was built with, re-pick the video-object variant by the
|
||||
// new graphic state and swap it onto the joint renderable IN PLACE. Execute()
|
||||
// re-reads graphicalObject each frame, so the wrecked mesh shows next frame.
|
||||
// No teardown: the component dtor does not cascade to children (L4VIDRND.cpp:104),
|
||||
// so a rebuild would leak -- the authentic behaviour is an in-place mesh swap.
|
||||
//
|
||||
void
|
||||
BTL4VideoRenderer::RemakeEntityRenderables(Entity *entity)
|
||||
{
|
||||
std::map<Entity*, MechRenderTree>::iterator tree_it =
|
||||
mMechRenderTrees.find(entity);
|
||||
if (tree_it == mMechRenderTrees.end())
|
||||
{
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[BTrender] RemakeEntity: no render tree for entity "
|
||||
<< (void*)entity << " (" << mMechRenderTrees.size()
|
||||
<< " tracked)\n" << std::flush;
|
||||
return; // tree not built yet -- Make will read the state
|
||||
}
|
||||
MechRenderTree &render_tree = tree_it->second;
|
||||
|
||||
JointedMover *jointed_mover = (JointedMover *)entity;
|
||||
EntitySegment::SkeletonType skeletonType =
|
||||
(EntitySegment::SkeletonType)render_tree.skeletonType;
|
||||
|
||||
EntitySegment::SegmentTableIterator segment_iterator(jointed_mover->segmentTable);
|
||||
EntitySegment *segment;
|
||||
int swapped = 0, checked = 0, mapped = 0;
|
||||
|
||||
while ((segment = segment_iterator.ReadAndNext()) != NULL)
|
||||
{
|
||||
if (segment->IsSiteSegment() != 0)
|
||||
continue;
|
||||
++checked;
|
||||
|
||||
int segment_slot = segment->GetIndex();
|
||||
std::map<int, HierarchicalDrawComponent*>::iterator r =
|
||||
render_tree.segRenderable.find(segment_slot);
|
||||
if (r == render_tree.segRenderable.end() || r->second == NULL)
|
||||
continue;
|
||||
++mapped;
|
||||
|
||||
//
|
||||
// Current graphic state for this segment (from its damage zone).
|
||||
//
|
||||
Enumeration seg_gstate = 0; // ExistsGraphicState
|
||||
int zone_index = segment->GetPrimaryDamageZone();
|
||||
if (zone_index >= 0 && zone_index < entity->damageZoneCount
|
||||
&& entity->damageZones[zone_index] != 0)
|
||||
seg_gstate = entity->damageZones[zone_index]->GetGraphicState();
|
||||
|
||||
if ((int)seg_gstate == render_tree.segGState[segment_slot])
|
||||
continue; // unchanged -- nothing to swap
|
||||
render_tree.segGState[segment_slot] = (int)seg_gstate;
|
||||
|
||||
//
|
||||
// Re-pick + load the segment's video-object variant for the new graphic
|
||||
// state (same construction as the initial build in MakeMechRenderables).
|
||||
//
|
||||
CString *object_name = segment->GetVideoObjectName(skeletonType, seg_gstate);
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[BTrender] seg '" << (const char *)segment->GetName()
|
||||
<< "' slot " << segment_slot << " -> gstate " << (int)seg_gstate
|
||||
<< " variant=" << (object_name ? (const char *)*object_name : "(none)")
|
||||
<< "\n" << std::flush;
|
||||
d3d_OBJECT *new_object = NULL;
|
||||
if (object_name != NULL)
|
||||
{
|
||||
char filename[44];
|
||||
strcpy(filename, (const char *)*object_name);
|
||||
int len = (int)strlen(filename);
|
||||
if (len >= 4)
|
||||
filename[len - 4] = '\0'; // strip ".bgf"
|
||||
strcat(filename, ".bgf");
|
||||
new_object = d3d_OBJECT::LoadObject(GetDevice(), filename);
|
||||
if (new_object == NULL && getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[BTrender] damaged variant '" << filename
|
||||
<< "' FAILED to load (expects VIDEO\\*.x)\n" << std::flush;
|
||||
if (new_object != NULL && strstr(filename, "tshd") != NULL)
|
||||
{
|
||||
new_object->SetIsShadow(1);
|
||||
for (int op = 0; op < new_object->GetDrawOpCount(); ++op)
|
||||
new_object->GetDrawOp(op)->alphaTest = true;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// GoneGraphicState (blown off): no mesh -> hide the segment. Destroyed/
|
||||
// Exists: swap to the variant if it loaded; otherwise keep the current
|
||||
// mesh (don't blank a segment merely because a damaged .bgf is missing).
|
||||
//
|
||||
if (new_object != NULL)
|
||||
r->second->SetDrawObj(new_object);
|
||||
else if ((int)seg_gstate == DamageZone::GoneGraphicState)
|
||||
r->second->SetDrawObj(NULL);
|
||||
|
||||
++swapped;
|
||||
}
|
||||
|
||||
if (swapped != 0 || getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[BTrender] RemakeEntity: " << swapped
|
||||
<< " mesh(es) swapped (" << mapped << " body segs mapped of "
|
||||
<< checked << " checked)\n" << std::flush;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// BTRemakeMechModel (sim-side bridge -- see btl4vid.hpp)
|
||||
//#############################################################################
|
||||
//
|
||||
// Reaches the live renderer and refreshes a mech's visible model after its
|
||||
// damage graphic state changed. Called from MechDeathHandler (sim TU). The
|
||||
// frame loop is single-threaded (sim + render share the main thread; only the
|
||||
// network RX socket runs on its own thread), so loading geometry here is safe.
|
||||
//
|
||||
void BTRemakeMechModel(Entity *entity)
|
||||
{
|
||||
if (entity == NULL || application == NULL)
|
||||
return;
|
||||
BTL4VideoRenderer *renderer =
|
||||
(BTL4VideoRenderer *)application->GetVideoRenderer();
|
||||
if (renderer != NULL)
|
||||
renderer->RemakeEntityRenderables(entity);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// BTReticleRenderable::AddWeapon
|
||||
|
||||
@@ -78,6 +78,7 @@ class DPLRenderer;
|
||||
#if !defined(NAMELIST_HPP)
|
||||
# include <namelist.hpp> // NameList / NameList::Entry
|
||||
#endif
|
||||
#include <map> // per-mech render-tree bookkeeping (RemakeEntity)
|
||||
|
||||
class Entity;
|
||||
class Mission;
|
||||
@@ -534,6 +535,40 @@ class BTReticleRenderable:
|
||||
void
|
||||
TearDownMaterialSubstitutionList(); // @004d11e8
|
||||
|
||||
//
|
||||
// RemakeEntity (damage-model swap). The 1996 binary drives a per-entity
|
||||
// render state machine (Make / RemakeEntity / DestroyEntity -- the state
|
||||
// name strings survive at .rdata 0x4e3f20). Only "Make" was ported (the
|
||||
// tree is built ONCE at entity creation, with fixed d3d_OBJECTs).
|
||||
// RemakeEntity is the model REFRESH: when a damage zone's graphic state
|
||||
// changes (a segment becomes Destroyed/Gone), re-pick each segment's video-
|
||||
// object variant by its zone graphic state (EntitySegment::GetVideoObjectName
|
||||
// is keyed by {skeleton, graphic_state}) and swap it onto the already-built
|
||||
// joint renderable IN PLACE (HierarchicalDrawComponent::Execute re-reads
|
||||
// graphicalObject each frame). No teardown: the component dtor does not
|
||||
// cascade to children (L4VIDRND.cpp:104) so a rebuild would leak -- the
|
||||
// authentic behaviour is an in-place mesh swap.
|
||||
//
|
||||
void
|
||||
RemakeEntityRenderables(Entity *entity);
|
||||
|
||||
protected:
|
||||
//
|
||||
// Per-mech render-tree bookkeeping so RemakeEntityRenderables can find each
|
||||
// segment's joint renderable (the dcs_array in MakeMechRenderables is local
|
||||
// and freed). Keyed by Entity*; one entry per built mech. segRenderable
|
||||
// maps a segment SLOT (EntitySegment::GetIndex) to its draw component;
|
||||
// segGState is the graphic state last applied to that slot so a swap only
|
||||
// reloads geometry on an actual state change.
|
||||
//
|
||||
struct MechRenderTree
|
||||
{
|
||||
int skeletonType; // EntitySegment::SkeletonType used at build
|
||||
std::map<int, HierarchicalDrawComponent*> segRenderable; // slot -> joint renderable
|
||||
std::map<int, int> segGState; // slot -> last applied graphic state
|
||||
};
|
||||
std::map<Entity*, MechRenderTree> mMechRenderTrees;
|
||||
|
||||
protected:
|
||||
//
|
||||
// Renderer-manager overrides
|
||||
@@ -598,6 +633,14 @@ class BTReticleRenderable:
|
||||
*tracer_zone; // [0x2e4] projectile tracers
|
||||
};
|
||||
|
||||
//
|
||||
// Sim-side bridge to RemakeEntityRenderables. MechDeathHandler (sim TU) calls
|
||||
// this when a mech's damage graphic state changes; defined in btl4vid.cpp, it
|
||||
// reaches the live renderer via l4_application->GetVideoRenderer(). A free
|
||||
// function so the sim TU needs no renderer header (just this extern).
|
||||
//
|
||||
extern void BTRemakeMechModel(Entity *entity);
|
||||
|
||||
#endif // BTL4VID_HPP
|
||||
|
||||
//===========================================================================//
|
||||
|
||||
@@ -486,7 +486,13 @@ void
|
||||
Mech::TakeDamageMessageHandler(TakeDamageMessage *message)
|
||||
{
|
||||
Check(message);
|
||||
DamageLookupTable *table = (DamageLookupTable *)Wword(0x111);
|
||||
// Maintain the last-attacker bookkeeping (mech[0x43c]): read by the
|
||||
// DamageZone LOD router (same-attacker redirect reuse, mechdmg.cpp:374)
|
||||
// and by the damage-band effect orientation. Was declared but never
|
||||
// WRITTEN -- a reconstruction gap; the natural authentic write site is
|
||||
// this handler (every damage message carries the inflictor). [T3]
|
||||
lastInflictingID = message->inflictingEntity;
|
||||
DamageLookupTable *table = (DamageLookupTable *)damageLookupTable; // named member (Wword absorbs!)
|
||||
if (message->invalidDamageZone && table != 0)
|
||||
{
|
||||
int zone = table->ResolveHit(message->damageData.impactPoint);
|
||||
@@ -1323,7 +1329,7 @@ Mech::Mech(
|
||||
// cached at mech[0x111] (byte 0x444). (Was an empty-name StandingAnimation
|
||||
// stub -> 0 rows; the real class is dmgtable.cpp.)
|
||||
//
|
||||
Wword(0x111) = 0;
|
||||
damageLookupTable = 0; // named member (Wword absorbs!)
|
||||
ResourceDescription *dzForName =
|
||||
MechFindResource(creation_message->resourceID,
|
||||
ResourceDescription::DamageZoneStreamResourceType); // type 0x14 (for its name)
|
||||
@@ -1339,7 +1345,7 @@ Mech::Mech(
|
||||
DynamicMemoryStream cylStream( // FUN_004032dc, offset 0
|
||||
cylRes->resourceAddress, cylRes->resourceSize, 0);
|
||||
DamageLookupTable *table = new DamageLookupTable(this, &cylStream); // FUN_0049ea48
|
||||
Wword(0x111) = (int)table;
|
||||
damageLookupTable = (int)table; // named member (Wword absorbs!)
|
||||
DEBUG_STREAM << "[cyl] table '" << dzForName->resourceName
|
||||
<< "' layers=" << table->LayerCount() << "\n" << std::flush;
|
||||
}
|
||||
@@ -1407,9 +1413,9 @@ Mech::~Mech()
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
|
||||
if (Wword(0x111) != 0) // cylinder hit-location table (STEP 6)
|
||||
if (damageLookupTable != 0) // cylinder hit-location table (STEP 6)
|
||||
{
|
||||
delete (DamageLookupTable *)Wword(0x111); // frees layers/slices/entries
|
||||
delete (DamageLookupTable *)damageLookupTable; // frees layers/slices/entries
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -488,7 +488,14 @@ protected:
|
||||
int heatLevel; // @0x518 this[0x146]
|
||||
int heatCapacity; // @0x51c this[0x147] = 0.6 * heatLevel
|
||||
Time creationTime; // @0x778 this[0x1de]
|
||||
int ammoExpended; // @0x444 this[0x111] (FUN_0049ea48)
|
||||
// Cylinder hit-location table (STEP 6) -- DamageLookupTable* cached by the
|
||||
// ctor from the type-0x1d resource (FUN_0049ea48 = the table ctor), read by
|
||||
// Mech::TakeDamageMessageHandler to resolve an unaimed (-1) hit's zone from
|
||||
// its impact point. MUST be a real named member: the old home Wword(0x111)
|
||||
// is the recon ABSORBER bank (BTVal stores nothing, reads 0) -> the cached
|
||||
// table silently vanished and every unaimed hit no-op'd. (Was mislabeled
|
||||
// "ammoExpended"; binary slot this[0x111] is this pointer.)
|
||||
int damageLookupTable; // @0x444 this[0x111] (FUN_0049ea48)
|
||||
int deathHandler; // @0x850 this[0x214] (FUN_0042a984)
|
||||
|
||||
// Three ref-counted creation-name objects (badge/color/insignia).
|
||||
|
||||
+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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,6 +575,24 @@ void
|
||||
{
|
||||
Subsystem *s = criticalSubsystems[i]->subsystemPlug.Resolve(); // +4
|
||||
MechCriticalSubsystem *cs = criticalSubsystems[i];
|
||||
|
||||
// UNBOUND-PLUG GUARD. Resolve() (FUN_00417ab4) returns the subsystem bound
|
||||
// to the DZSlot, or 0 if plug+8 was never wired. In the 1995 binary every
|
||||
// critical subsystem exists and is bound, so it dereferences s with no check.
|
||||
// In this reconstruction a critical subsystem can be UNBOUND (its type is not
|
||||
// yet built, or its slot never connected), leaving s == 0 -> the original
|
||||
// code AV'd here (mov [edx+0xc], edx=0) as soon as a zone with such a plug was
|
||||
// destroyed. Skip the unbound entry rather than crash; log it so the missing
|
||||
// binding can be tracked. NOT a behavioural stand-in: a bound plug takes the
|
||||
// authentic path below unchanged. [T3 -- see open-questions: crit-subsys binding]
|
||||
if (s == 0)
|
||||
{
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[deathfx] crit-subsys " << i << "/" << criticalSubsystemCount
|
||||
<< " UNBOUND (plug not wired) -- skipped\n" << std::flush;
|
||||
continue;
|
||||
}
|
||||
|
||||
Mech__DamageZone *zone = (Mech__DamageZone *)((SubProxy2 *)s)->damageZone; // subsystem[0x38] @0xE0
|
||||
|
||||
zone->damageLevel += (cs->damagePercentage - cs->damagePercentageUsed);
|
||||
@@ -911,7 +929,12 @@ void
|
||||
// live): spawns the descriptor's explosion resource at the mech. The authentic
|
||||
// path dispatches a class-5 message to the effect manager (app+0x38 -> the 0xBD3
|
||||
// SubsystemMessageManager, unreconstructed); we use the established Explosion port.
|
||||
extern void BTSpawnDamageEffect(Mech *mech, int effect_resource);
|
||||
extern void BTSpawnDamageEffect(Mech *mech, int effect_resource, int segment_index);
|
||||
|
||||
// Render bridge (btl4vid.cpp): swap a mech's wrecked segment meshes onto its
|
||||
// already-built render tree when a damage zone's graphic state changes (the
|
||||
// unported "RemakeEntity" render state). Entity* param -> Mech* binds fine.
|
||||
extern void BTRemakeMechModel(Entity *entity);
|
||||
|
||||
MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984
|
||||
: owner(mech)
|
||||
@@ -960,8 +983,27 @@ void
|
||||
}
|
||||
if (d != 0)
|
||||
{
|
||||
BTSpawnDamageEffect(owner, d->effectResource); // explosion at the zone
|
||||
zone->ApplyDamageGraphicState(d->graphicState); // destroyed skin
|
||||
BTSpawnDamageEffect(owner, d->effectResource, // explosion AT the zone's
|
||||
zone->segmentIndex); // segment (world position)
|
||||
zone->ApplyDamageGraphicState(d->graphicState); // destroyed skin (graphic state)
|
||||
// A graphic-state change means the segment's MODEL changed (intact ->
|
||||
// destroyed variant, keyed by GetVideoObjectName(skl, gstate)). Fire
|
||||
// the engine's model-rebuild flag so the renderer re-runs
|
||||
// MakeMechRenderables and loads the destroyed segment geometry -- the
|
||||
// same mechanism CulturalIcons use to visibly deform on damage
|
||||
// (CULTURAL.cpp:91; EXPTBL.cpp:549 does this per zone in the engine's
|
||||
// ExplosionTable). Without this the tree stays as built at spawn (intact).
|
||||
// The render's "RemakeEntity" state (which consumes this flag) was
|
||||
// NOT ported -- the mech tree is built once at spawn -- so we drive the
|
||||
// equivalent directly: set the flag (authentic signal; also flags the
|
||||
// zone for net damage replication) AND call the render bridge, which
|
||||
// swaps the wrecked segment mesh onto the already-built tree in place.
|
||||
// The single-threaded frame loop makes the immediate swap safe.
|
||||
if (d->graphicState != DamageZone::ExistsGraphicState)
|
||||
{
|
||||
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
||||
BTRemakeMechModel(owner); // RemakeEntity: swap in the destroyed mesh
|
||||
}
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[deathfx] zone " << i << " level " << level
|
||||
<< " -> effect " << d->effectResource << " gstate " << d->graphicState
|
||||
|
||||
Reference in New Issue
Block a user