BT410 Phase 5.3.16: targeting + damage -- two-mech fights are LIVE
The simulation is now a FIGHT. Mech target slot (+0x388), authentic Loaded->Firing gate (viewFireEnable && HasActiveTarget), UpdateTargeting range refresh, and SendDamage via the engine TakeDamageMessage. MECHDMG.CPP is BORN against the AUTHENTIC surviving CODE/BT/BT/MECHDMG.HPP: the Mech ctor's Pass-3 hull zone fill constructs Mech__DamageZone per zone (the Entity base only allocates the raw pointer array -- unfilled slots were crash #1; a base-class fill parses each record short and skews the stream -- crash #2). Streamed ctor parses the full BT tail (flags / Scalar-weighted criticals with the Master+Dynamic plug gate / LOD redirect table) and normalizes the armor economy: scale[type]=1/(raw x armorPoints), legs halved -- BT411-verified @0049ce50. Live two-mech verification (BT_SPAWN_ENEMY harness in DropZoneReply): 20 named hull zones stream on both mechs; PPC lands 11.77/hit (12 x chargeRatio^2, type 4), ER-M laser 3.43 (type 3), SRM6 salvos 5.83 x 6 rounds on independent zones (type 2, one message per missile -- burstCount is NOT in the zone formula); repeat hits climb linearly, leg zones absorb at half scale, zones reach 1.0 Burning/Destroyed. No-target and novice regressions hold (weapons load but never fire without a target). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -304,6 +304,62 @@ void
|
||||
//
|
||||
AlwaysExecute();
|
||||
deathCount = 0;
|
||||
|
||||
//
|
||||
// DEV harness (BT_SPAWN_ENEMY=1): spawn a second, unowned mech 150 u
|
||||
// down +Z from the drop zone through the SAME make machinery (its
|
||||
// Entity ctor self-registers with the host manager; no player link, so
|
||||
// its gates read dev-permissive) and hand it to our mech as the
|
||||
// TARGET. The authentic target writer is the reticle pick (the
|
||||
// render/mech4 wave); this gives the fire/damage path a live opponent
|
||||
// headlessly.
|
||||
//
|
||||
if (getenv("BT_SPAWN_ENEMY")
|
||||
&& playerVehicle->GetClassID() == (Entity::ClassID)MechClassID)
|
||||
{
|
||||
HostManager *host_manager = application->GetHostManager();
|
||||
Check(host_manager);
|
||||
ResourceDescription *mech_res =
|
||||
application->GetResourceFile()->FindResourceDescription(
|
||||
playerMission->GetGameModel(),
|
||||
ResourceDescription::ModelListResourceType
|
||||
);
|
||||
Check(mech_res);
|
||||
mech_res->Lock();
|
||||
|
||||
Origin enemy_location = message->dropZoneLocation;
|
||||
enemy_location.linearPosition.z += 150.0f;
|
||||
|
||||
Mech::MakeMessage
|
||||
create_enemy(
|
||||
Mech::MakeMessageID,
|
||||
sizeof(Mech::MakeMessage),
|
||||
EntityID(host_manager->GetLocalHostID()),
|
||||
(Entity::ClassID)MechClassID,
|
||||
EntityID::Null,
|
||||
mech_res->resourceID,
|
||||
Mech::DefaultFlags,
|
||||
enemy_location,
|
||||
Motion::Identity,
|
||||
Motion::Identity,
|
||||
playerMission->GetBadgeName(),
|
||||
playerMission->GetColorName(),
|
||||
((BTMission *)playerMission)->GetPatchName()
|
||||
);
|
||||
|
||||
mech_res->Unlock();
|
||||
|
||||
Mech *enemy = Mech::Make(&create_enemy);
|
||||
Register_Object(enemy);
|
||||
((Mech *)playerVehicle)->SetTargetEntity(enemy);
|
||||
|
||||
if (getenv("BT_MECH_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[enemy] spawned " << (void *)enemy
|
||||
<< " at z+150, zones=" << enemy->damageZoneCount
|
||||
<< " -> target locked" << endl << flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (deathCount == message->deathCount)
|
||||
{
|
||||
|
||||
@@ -161,3 +161,14 @@ Public accessors IsSimLive()/IsHeatModelOn()/IsAdvancedDamageOn(); a gated
|
||||
`[exp]` sentinel prints the derived flags per master player. Consumers:
|
||||
HeatSink::HeatModelActive() (HEAT.CPP), ProjectileWeapon::LiveFireEnabled()
|
||||
(PROJWEAP.CPP) -- both walk mech->GetPlayerLink(), NULL-permissive.
|
||||
|
||||
## 5.3.16 — BT_SPAWN_ENEMY dev harness
|
||||
|
||||
DropZoneReplyMessageHandler, after the fresh-spawn block: `BT_SPAWN_ENEMY=1`
|
||||
clones the CreatePlayerVehicle MakeMessage recipe (same game-model resource,
|
||||
badge/color/patch, `Mech::DefaultFlags`) at dropZone+150 on Z, calls
|
||||
`Mech::Make()` directly (the Entity ctor self-registers + uniquifies the ID)
|
||||
and sets it as the player mech's target. The enemy has NO player link, so its
|
||||
experience gates read dev-permissive; it never fires back (no target of its
|
||||
own — set one for a mutual fight). DEV-ONLY: the real opponent path is the
|
||||
network mesh / mission roster.
|
||||
|
||||
@@ -309,10 +309,18 @@ void
|
||||
* chargeRatio * chargeRatio;
|
||||
|
||||
//
|
||||
// The damage record carries this shot's portion (the submission at the
|
||||
// owner's target joins the targeting/damage wave).
|
||||
// The damage submission (binary @004bace8 tail): the shot's portion rides
|
||||
// the inherited Damage record to the owner's target when it is inside the
|
||||
// weapon's effective range. (The beam build / impact point join the
|
||||
// render wave.)
|
||||
//
|
||||
damageData.damageAmount = damagePortion;
|
||||
damageData.burstCount = 1;
|
||||
if (targetWithinRange && owner != NULL
|
||||
&& owner->GetTargetEntity() != NULL)
|
||||
{
|
||||
SendDamage(owner->GetTargetEntity(), damageData);
|
||||
}
|
||||
|
||||
if (HeatModelActive())
|
||||
{
|
||||
@@ -417,6 +425,8 @@ void
|
||||
|
||||
Logical fireEdge = CheckFireEdge();
|
||||
|
||||
targetWithinRange = UpdateTargeting();
|
||||
|
||||
switch (GetWeaponState())
|
||||
{
|
||||
case FiringState:
|
||||
@@ -433,7 +443,12 @@ void
|
||||
case LoadedState:
|
||||
if (fireEdge)
|
||||
{
|
||||
if (viewFireEnable)
|
||||
//
|
||||
// The authentic Loaded->Firing gate: armed in the current look
|
||||
// view AND a target under the reticle -- a denied shot is the
|
||||
// one-frame blip, no discharge.
|
||||
//
|
||||
if (viewFireEnable && HasActiveTarget())
|
||||
{
|
||||
weaponAlarm.SetLevel(FiringState);
|
||||
FireWeapon();
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include <mechmppr.hpp> // MechControlsMapper -- the drive reads its demands
|
||||
#include <joint.hpp> // Joint / JointSubsystem -- ResolveJoint
|
||||
#include <segment.hpp> // EntitySegment -- the skeleton segment table
|
||||
#include <mechdmg.hpp> // Mech::DamageZone -- the hull zone fill (Pass 3)
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
@@ -165,6 +166,7 @@ Mech::Mech(
|
||||
eyepointRotation = EulerAngles::Identity;
|
||||
lookPitch = 0.0f;
|
||||
lookYaw = 0.0f;
|
||||
targetEntity = NULL;
|
||||
|
||||
//
|
||||
// Look-view angles: defaults until the GameModel read below overrides them
|
||||
@@ -176,7 +178,7 @@ Mech::Mech(
|
||||
lookBackAngle = 0.0f;
|
||||
|
||||
{
|
||||
for (int i = 0; i < 202; ++i)
|
||||
for (int i = 0; i < 201; ++i)
|
||||
{
|
||||
reservedState[i] = 0;
|
||||
}
|
||||
@@ -459,6 +461,64 @@ Mech::Mech(
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------------
|
||||
// Fill the hull damage-zone array. The Entity base ctor only READS the
|
||||
// zone count and allocates the raw POINTER array (entity.cpp:1032) -- the
|
||||
// derived entity must construct the streamed DamageZone objects into the
|
||||
// slots itself (the CulturalIcon ctor is the surviving reference,
|
||||
// cultural.cpp:349). LOAD-BEARING: an unfilled slot is heap garbage, and
|
||||
// the first TakeDamageMessage that lands on it calls through a trash
|
||||
// vtable (crash = EIP inside the heap). Runs AFTER the segment walk
|
||||
// because the streamed DamageZone ctor resolves its effect-site segments
|
||||
// via GetSegment() on JointedMover-derived owners.
|
||||
//-----------------------------------------------------------------------
|
||||
//
|
||||
if (damageZones != NULL && damageZoneCount > 0)
|
||||
{
|
||||
ResourceDescription *dmg_res =
|
||||
application->GetResourceFile()->SearchList(
|
||||
resourceID,
|
||||
ResourceDescription::DamageZoneStreamResourceType
|
||||
);
|
||||
Check(dmg_res);
|
||||
dmg_res->Lock();
|
||||
DynamicMemoryStream damage_stream(
|
||||
dmg_res->resourceAddress,
|
||||
dmg_res->resourceSize
|
||||
);
|
||||
damage_stream.AdvancePointer(sizeof(damageZoneCount));
|
||||
//
|
||||
// Every entry is the MECH-specific zone subclass (the class-scope
|
||||
// DamageZone typedef = Mech__DamageZone, binary ctor @0049ce50): its
|
||||
// streamed ctor chains the engine base parse and consumes the BT
|
||||
// per-zone TAIL (flags / criticals / LOD redirects) -- a base-class
|
||||
// fill parses zone 0 short and skews every following zone.
|
||||
//
|
||||
{
|
||||
for (int dz = 0; dz < damageZoneCount; ++dz)
|
||||
{
|
||||
damageZones[dz] = new DamageZone(this, dz, &damage_stream);
|
||||
Register_Object(damageZones[dz]);
|
||||
}
|
||||
for (int dp = 0; dp < damageZoneCount; ++dp)
|
||||
{
|
||||
((DamageZone *)damageZones[dp])->SetLODParentPointers();
|
||||
}
|
||||
}
|
||||
dmg_res->Unlock();
|
||||
|
||||
if (getenv("BT_MECH_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[mech] damage zones streamed: " << damageZoneCount;
|
||||
for (int zz = 0; zz < damageZoneCount; ++zz)
|
||||
{
|
||||
DEBUG_STREAM << (zz ? "," : " [") << damageZones[zz]->damageZoneName;
|
||||
}
|
||||
DEBUG_STREAM << "]" << endl << flush;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Seed the condenser flow shares: every valve spawns at 1, so the initial
|
||||
// recompute yields equal shares across the bank (a MoveValve press
|
||||
|
||||
@@ -323,6 +323,17 @@
|
||||
const EulerAngles&
|
||||
GetEyepointRotation() const { Check(this); return eyepointRotation; }
|
||||
|
||||
//
|
||||
// The current target (the binary's mech target slot @0x388): read by
|
||||
// the weapons' HasActiveTarget / UpdateTargeting and the fire path.
|
||||
// The authentic writer is the reticle targeting step (mech4, the
|
||||
// render wave); the dev harness sets it directly.
|
||||
//
|
||||
Entity*
|
||||
GetTargetEntity() const { Check(this); return targetEntity; }
|
||||
void
|
||||
SetTargetEntity(Entity *target) { Check(this); targetEntity = target; }
|
||||
|
||||
//
|
||||
// Look-state commit (called by the controls mapper on a look-button
|
||||
// state change): re-aim the eyepoint from the model's authored look
|
||||
@@ -399,6 +410,7 @@
|
||||
EulerAngles eyepointRotation;
|
||||
Scalar lookPitch;
|
||||
Scalar lookYaw;
|
||||
Entity *targetEntity;
|
||||
|
||||
//
|
||||
// The model's authored look-view angles (rad; deg in the resource).
|
||||
@@ -413,7 +425,7 @@
|
||||
// mech2/mech3/mech4 simulation. Reserved until that path is
|
||||
// reconstructed with named fields.
|
||||
//
|
||||
int reservedState[202];
|
||||
int reservedState[201];
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -365,3 +365,18 @@ event queue, so the mech never received a directly-dispatched message -- the
|
||||
PlayerLink bind silently never landed and every player-experience gate read a
|
||||
NULL player (dev-permissive heat-always-on) until this line was restored.
|
||||
Found while verifying the novice-mode heat lockout end-to-end.
|
||||
|
||||
## 5.3.16 — the target slot + hull zone fill (Pass 3)
|
||||
|
||||
- `Entity *targetEntity` @mech+0x388 (Get/SetTargetEntity; ctor NULLs it;
|
||||
reservedState [202]→[201]). The authentic WRITER is the reticle pick
|
||||
(render/mech4 wave); the BT_SPAWN_ENEMY harness (BTPLAYER.CPP) sets it for
|
||||
headless fights.
|
||||
- Hull damage-zone fill after the model-param block: re-open the type-0x14
|
||||
DamageZoneStream, skip the count word the Entity base ctor consumed, and
|
||||
construct `new DamageZone(this, dz, &stream)` per zone — the CLASS-SCOPE
|
||||
typedef = `Mech__DamageZone` (authentic CODE/BT/BT/MECHDMG.HPP), whose ctor
|
||||
parses the full BT per-zone record. Then the SetLODParentPointers pass over
|
||||
all zones (binary Pass-3 order, FUN_004a1674). Full forensics in
|
||||
MECHDMG.NOTES.md — the Entity ctor ONLY allocates the pointer array; an
|
||||
unfilled or base-class fill is the 5.3.16 crash pair.
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
//===========================================================================//
|
||||
// File: mechdmg.cpp //
|
||||
// Project: BattleTech Brick: Mech damage //
|
||||
// Contents: Mech::DamageZone -- the hull zone armor economy //
|
||||
//---------------------------------------------------------------------------//
|
||||
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
||||
// All Rights reserved worldwide //
|
||||
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
||||
//===========================================================================//
|
||||
//
|
||||
// The class interface is the AUTHENTIC surviving CODE/BT/BT/MECHDMG.HPP
|
||||
// (06/05/95 JM). The bodies here are reconstructions: the streamed ctor /
|
||||
// SetLODParentPointers / dtor follow the BT411-verified binary parse
|
||||
// (@0049ce50 / @0049d1d0), while TakeDamage and SetGraphicState are STAGED
|
||||
// pass-throughs to the engine base -- the LOD artifact router (@0049c690),
|
||||
// the critical destruction cascade and the death-handler graphics are the
|
||||
// mechdmg cascade wave (see MECHDMG.NOTES.md).
|
||||
//
|
||||
|
||||
#include <bt.hpp>
|
||||
#pragma hdrstop
|
||||
|
||||
#if !defined(MECHDMG_HPP)
|
||||
# include <mechdmg.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(MECH_HPP)
|
||||
# include <mech.hpp>
|
||||
#endif
|
||||
|
||||
const int
|
||||
Mech__DamageZone::NullDamageZone = -1;
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// MechCriticalSubsystem (binary ctor @0049dd18): one critical-subsystem
|
||||
// entry of a hull zone -- the plug binds the roster subsystem this zone
|
||||
// destroys when its structure runs out; weight / percentage stream in the
|
||||
// zone ctor below.
|
||||
//#############################################################################
|
||||
//
|
||||
MechCriticalSubsystem::MechCriticalSubsystem(DamageZone *owner):
|
||||
subsystemPlug(owner)
|
||||
{
|
||||
Check(owner);
|
||||
criticalWeight = 0.0f;
|
||||
damagePercentage = 0.0f;
|
||||
damagePercentageUsed = 0.0f;
|
||||
}
|
||||
|
||||
MechCriticalSubsystem::~MechCriticalSubsystem()
|
||||
{
|
||||
}
|
||||
|
||||
Logical
|
||||
MechCriticalSubsystem::TestInstance() const
|
||||
{
|
||||
return True;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Mech::DamageZone construction (binary @0049ce50).
|
||||
//
|
||||
// Chains to the engine DamageZone streamed ctor (name / effect sites /
|
||||
// defaultArmorPoints / damageScale[5] / material lists -- DAMAGE.CPP:234),
|
||||
// then consumes the BT mech-specific TAIL of the same per-zone record:
|
||||
//
|
||||
// Logical descendOnDestruction, destroySiblingsOnDestruction;
|
||||
// int segmentIndex;
|
||||
// Logical leftLeg, rightLeg, vitalDamageZone;
|
||||
// int criticalSubsystemCount;
|
||||
// critCount x { Scalar criticalWeight; Scalar damagePercentage;
|
||||
// int subsystemIndex -> subsystemPlug }
|
||||
// int redirectCount; redirectCount x int childZoneIndex
|
||||
//
|
||||
// Parsing the FULL record is LOAD-BEARING: the Mech ctor streams all hull
|
||||
// zones sequentially from ONE stream, so a short read here skews every
|
||||
// following zone (its effect-site indices then read garbage and
|
||||
// GetSegment() returns NULL -- the 5.3.16 bring-up crash).
|
||||
//
|
||||
// THE ARMOR ECONOMY (BT411 combat-damage, verified vs the binary): the
|
||||
// stream authors armor POINTS; each damageScale cell is normalized to a
|
||||
// damage-per-point fraction scale[type] = 1/(raw[type] * armorPoints)
|
||||
// (cells already equal to 1/armorPoints pass through), then LEG zones halve
|
||||
// every type's scale. The engine base TakeDamage applies
|
||||
// damageLevel += amount * damageScale[type] (1.0 = destroyed).
|
||||
//#############################################################################
|
||||
//
|
||||
Mech__DamageZone::Mech__DamageZone(
|
||||
Mech *mech,
|
||||
int damage_zone_index,
|
||||
MemoryStream *stream
|
||||
):
|
||||
DamageZone(mech, damage_zone_index, stream),
|
||||
redirectTable(NULL, False),
|
||||
parentArtifactZone(NULL)
|
||||
{
|
||||
Check(mech);
|
||||
Check(stream);
|
||||
|
||||
lastHitZone = 0;
|
||||
lastDamageTime = 0.0f;
|
||||
lastInflicting = mech->GetEntityID();
|
||||
criticalWeightSum = 0.0f;
|
||||
criticalSubsystems = NULL;
|
||||
|
||||
//
|
||||
// The streamed scalar/flag block (binary order @0049cf00-0049cf5c).
|
||||
//
|
||||
*stream >> descendOnDestruction;
|
||||
*stream >> destroySiblingsOnDestruction;
|
||||
*stream >> segmentIndex;
|
||||
*stream >> leftLeg;
|
||||
*stream >> rightLeg;
|
||||
*stream >> vitalDamageZone;
|
||||
*stream >> criticalSubsystemCount;
|
||||
|
||||
//
|
||||
// Normalize the per-type armor scales in place (binary @0049cf60,
|
||||
// constants 1e-4 / 0.5).
|
||||
//
|
||||
{
|
||||
for (int type = 0; type < Damage::DamageTypeCount; ++type)
|
||||
{
|
||||
Scalar even = 1.0f / defaultArmorPoints;
|
||||
Scalar diff = damageScale[type] - even;
|
||||
if (diff < 0.0f)
|
||||
{
|
||||
diff = -diff;
|
||||
}
|
||||
if (diff > 1.0e-4f)
|
||||
{
|
||||
damageScale[type] =
|
||||
1.0f / (damageScale[type] * defaultArmorPoints);
|
||||
}
|
||||
if (leftLeg || rightLeg)
|
||||
{
|
||||
damageScale[type] *= 0.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The critical-subsystem table. The streamed roster index binds the
|
||||
// subsystem plug on the MASTER mech only (binary gate @0049d0e1:
|
||||
// MasterInstance + DynamicFlag -- replicant plugs stay unbound, subsystem
|
||||
// criticals are master-authoritative). A NULL roster slot is left
|
||||
// unbound: slot 0 (the control mapper) installs post-ctor and
|
||||
// Link::AddToPlug derefs the plug.
|
||||
//
|
||||
if (criticalSubsystemCount > 0)
|
||||
{
|
||||
criticalSubsystems =
|
||||
new MechCriticalSubsystem *[criticalSubsystemCount];
|
||||
Register_Pointer(criticalSubsystems);
|
||||
for (int cc = 0; cc < criticalSubsystemCount; ++cc)
|
||||
{
|
||||
criticalSubsystems[cc] = new MechCriticalSubsystem(this);
|
||||
Register_Object(criticalSubsystems[cc]);
|
||||
|
||||
int subsystem_index = -1;
|
||||
*stream >> criticalSubsystems[cc]->criticalWeight;
|
||||
*stream >> criticalSubsystems[cc]->damagePercentage;
|
||||
*stream >> subsystem_index;
|
||||
criticalWeightSum += criticalSubsystems[cc]->criticalWeight;
|
||||
|
||||
if (
|
||||
mech->GetInstance() == Entity::MasterInstance
|
||||
&& mech->IsDynamic()
|
||||
&& subsystem_index >= 0
|
||||
&& subsystem_index < mech->GetSubsystemCount()
|
||||
)
|
||||
{
|
||||
Subsystem *target = mech->GetSubsystem(subsystem_index);
|
||||
if (target != NULL)
|
||||
{
|
||||
criticalSubsystems[cc]->subsystemPlug.Add(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// The LOD redirect table: an ARTIFACT zone (low-LOD hull shell) lists
|
||||
// the real child zones a hit on it lands on.
|
||||
//
|
||||
int redirect_count = 0;
|
||||
*stream >> redirect_count;
|
||||
{
|
||||
for (int rr = 0; rr < redirect_count; ++rr)
|
||||
{
|
||||
int child_zone = 0;
|
||||
*stream >> child_zone;
|
||||
IntegerPlug *plug = new IntegerPlug(child_zone);
|
||||
Register_Object(plug);
|
||||
redirectTable.AddValue(plug, child_zone);
|
||||
}
|
||||
}
|
||||
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
Mech__DamageZone::~Mech__DamageZone()
|
||||
{
|
||||
if (criticalSubsystems != NULL)
|
||||
{
|
||||
for (int cc = 0; cc < criticalSubsystemCount; ++cc)
|
||||
{
|
||||
Unregister_Object(criticalSubsystems[cc]);
|
||||
delete criticalSubsystems[cc];
|
||||
}
|
||||
Unregister_Pointer(criticalSubsystems);
|
||||
delete [] criticalSubsystems;
|
||||
}
|
||||
|
||||
DamageZoneIndexTableIterator iterator(redirectTable);
|
||||
iterator.DeletePlugs();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// SetLODParentPointers (binary @0049d1d0). Run by the Mech ctor after EVERY
|
||||
// zone of the mech exists: each redirect child's parentArtifactZone slot
|
||||
// points back at this artifact zone (the parent-level damage recompute walks
|
||||
// it -- cascade wave).
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
Mech__DamageZone::SetLODParentPointers()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
Mech *mech = (Mech *)GetOwningSimulation();
|
||||
Check(mech);
|
||||
|
||||
DamageZoneIndexTableIterator iterator(redirectTable);
|
||||
IntegerPlug *plug;
|
||||
while ((plug = iterator.ReadAndNext()) != NULL)
|
||||
{
|
||||
int child = *plug;
|
||||
if (child >= 0 && child < mech->damageZoneCount)
|
||||
{
|
||||
Mech__DamageZone *child_zone =
|
||||
(Mech__DamageZone *)mech->damageZones[child];
|
||||
child_zone->parentArtifactZone.Add(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Damage entry. STAGED: the engine base armor model applies directly --
|
||||
// the authentic override (@0049c690) first routes ARTIFACT hits through the
|
||||
// LOD redirect table to a real child zone, recomputes the artifact parent's
|
||||
// displayed level, rolls critical-subsystem hits and updates the leg-failure
|
||||
// state. All of that is the mechdmg cascade wave; the armor economy
|
||||
// (amount x normalized scale, clamp, Burning/Destroyed at 1.0) is live NOW.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
Mech__DamageZone::TakeDamage(Damage &damage)
|
||||
{
|
||||
Check(this);
|
||||
DamageZone::TakeDamage(damage);
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Graphic state. STAGED pass-through: the authentic override also flips the
|
||||
// zone's skeleton-segment video objects to the destroyed skins (cascade
|
||||
// wave); the base keeps the state machine + changed flags correct for the
|
||||
// network update records.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
Mech__DamageZone::SetGraphicState(Enumeration new_state)
|
||||
{
|
||||
Check(this);
|
||||
DamageZone::SetGraphicState(new_state);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
# MECHDMG.CPP — Mech::DamageZone (hull zone armor economy)
|
||||
|
||||
**Status: PARTIAL (5.3.16) — streamed ctor / LOD parent pass / dtor reconstructed; TakeDamage + SetGraphicState are staged base pass-throughs.**
|
||||
|
||||
## The header is AUTHENTIC
|
||||
|
||||
`CODE/BT/BT/MECHDMG.HPP` (06/05/95 JM) **survives in the archive** and wins the
|
||||
include path (`$C/BT` precedes `$S/BT`), so this TU compiles against the real
|
||||
1995 interface: `MechCriticalSubsystem` with a `SlotOf<Subsystem*>` plug and
|
||||
**Scalar** criticalWeight, `Logical` zone flags, `TableOf<PlugOf<int>*,int>`
|
||||
redirectTable, `SlotOf<Mech__DamageZone*> parentArtifactZone`. Only the .CPP is
|
||||
a reconstruction. (A from-scratch staged header was written and then DELETED
|
||||
the same session once the survivor was found — do not resurrect it.)
|
||||
|
||||
## Why this TU exists at wave 5.3.16 (the bring-up crash)
|
||||
|
||||
`Entity::Entity` (engine, ENTITY.CPP:1032) only reads the zone COUNT from the
|
||||
type-0x14 DamageZoneStream and allocates the raw `damageZones[count]` POINTER
|
||||
array — **it never constructs the zones**. The derived entity must fill the
|
||||
slots itself (CulturalIcon = the surviving engine reference, CULTURAL.CPP:349;
|
||||
the Mech = binary Pass 3, FUN_004a1674). Two crashes taught this:
|
||||
|
||||
1. Unfilled slots are heap garbage → the FIRST TakeDamageMessage dispatched at
|
||||
a mech called through a trash vtable (exception 0E, EIP inside the heap).
|
||||
2. Filling with the engine BASE `::DamageZone` streamed ctor parses each zone
|
||||
record SHORT (the BT tail below goes unread) → the stream skews → the next
|
||||
zone's effect-site indices read garbage → `GetSegment()` NULL →
|
||||
`Link::AddToPlug(NULL)` (exception 0E at AddToPlug+9). The class-scope
|
||||
`DamageZone` typedef (= `Mech__DamageZone`) in the authentic mech ctor is
|
||||
the load-bearing signal: mech zones are ALWAYS the subclass.
|
||||
|
||||
## The per-zone stream record (type 0x14, after the count word)
|
||||
|
||||
Engine base part (DAMAGE.CPP:234, authentic order, VERIFIED identical RP↔BT411):
|
||||
`CString name` · per 5 effect-site classes `{int n; n×int segIdx}` (JointedMover
|
||||
owners resolve them via GetSegment; non-jointed read 5 filler ints) ·
|
||||
`Scalar defaultArmorPoints` · `Scalar damageScale[5]` · material lists.
|
||||
|
||||
BT mech tail (this ctor, BT411-verified @0049ce50):
|
||||
`Logical descend, destroySiblings` · `int segmentIndex` · `Logical leftLeg,
|
||||
rightLeg, vital` · `int critCount` · critCount × `{Scalar weight; Scalar
|
||||
damagePct; int rosterIndex}` · `int redirectCount` · redirectCount × `int
|
||||
childZone`.
|
||||
|
||||
## The armor economy (BT411 combat-damage.md, binary-verified)
|
||||
|
||||
- Stream authors armor POINTS; ctor normalizes in place:
|
||||
`scale[type] = 1/(raw[type]×armorPoints)` (cells already `1/armorPoints`
|
||||
pass through, tolerance 1e-4); leg zones halve every type's scale.
|
||||
- Base `DamageZone::TakeDamage` then applies
|
||||
`damageLevel += amount × scale[type]`, clamp [0,1], 1.0 = Burning/Destroyed.
|
||||
- `damageType` indexes the 5 cells DIRECTLY: 0 Collision / 1 Ballistic /
|
||||
2 Explosive / 3 Laser / 4 Energy. **burstCount is NOT in the formula** —
|
||||
one message = one application (it only feeds gyro bounce + splash falloff).
|
||||
- Live-verified 5.3.16 (two-mech fight): PPC 11.77/hit type 4 (authored 12 ×
|
||||
chargeRatio²), ER-M laser 3.43 type 3, SRM 5.83×6 type 2; repeat hits climb
|
||||
linearly; 70-pt leg zones absorb at the halved scale; zones reach 1.0.
|
||||
|
||||
## Crit table binding
|
||||
|
||||
Streamed roster index → `subsystemPlug.Add(GetSubsystem(idx))`, gated
|
||||
MasterInstance + DynamicFlag (binary @0049d0e1 — replicant plugs stay unbound,
|
||||
criticals are master-authoritative) AND NULL-guarded: roster slot 0 (control
|
||||
mapper) installs post-ctor, slot 1 (voltage bus) can be empty, and
|
||||
`Link::AddToPlug` derefs the plug.
|
||||
|
||||
## Staged / deferred (the mechdmg cascade wave)
|
||||
|
||||
- `TakeDamage` override = base pass-through. Authentic (@0049c690): artifact
|
||||
zones route hits through the redirect table to a real child
|
||||
(`LODDamageRouter`/`RandomRedirect`/`lastHitZone`), parent recomputes its
|
||||
displayed level (`UpdateLODDamageLevel`), critical rolls (`CriticalHit`),
|
||||
generator shorting, leg-failure state.
|
||||
- `SetGraphicState` override = base pass-through (authentic also flips the
|
||||
segment video objects to destroyed skins).
|
||||
- Declared-not-defined (unreferenced, so they link): `LODDamageRouter`,
|
||||
`RandomRedirect`, `UpdateLODDamageLevel`, `ShortAttachedGenerators`,
|
||||
`SendSubsystemDamage`, `RecurseSegmentTable`, `CriticalHit`,
|
||||
`CreateStreamedDamageZone`, `GetSegmentIndex`.
|
||||
- `lastHitZone` staged 0 (authentic seeds via RandomRedirect when redirects
|
||||
exist); `lastDamageTime` staged 0 (authentic = TheTime tick / TicksPerSecond).
|
||||
- The type-0x1e critical-descriptor stream + MechDeathHandler (BT411
|
||||
mech.cpp Pass-3 tail) are NOT loaded yet — same cascade wave.
|
||||
@@ -25,6 +25,10 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#if !defined(RANDOM_HPP)
|
||||
# include <random.hpp>
|
||||
#endif
|
||||
|
||||
Derivation
|
||||
MechWeapon::ClassDerivations(
|
||||
PoweredSubsystem::ClassDerivations,
|
||||
@@ -261,14 +265,93 @@ void
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// SendDamage Deliver this weapon's damage to a struck entity. The damage/
|
||||
// networking path is not yet reconstructed.
|
||||
// HasActiveTarget -- the owner mech's target slot (binary owner+0x388).
|
||||
//#############################################################################
|
||||
//
|
||||
Logical
|
||||
MechWeapon::HasActiveTarget()
|
||||
{
|
||||
Check(this);
|
||||
return (owner != NULL && owner->GetTargetEntity() != NULL)
|
||||
? True : False;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// UpdateTargeting -- refresh the range to the current target and the
|
||||
// within-range flag (binary @004b9bdc). effectiveRange is the (heat-degraded,
|
||||
// later) weapon range.
|
||||
//#############################################################################
|
||||
//
|
||||
Logical
|
||||
MechWeapon::UpdateTargeting()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
Entity *target = (owner != NULL) ? owner->GetTargetEntity() : NULL;
|
||||
if (target == NULL)
|
||||
{
|
||||
targetWithinRange = False;
|
||||
return False;
|
||||
}
|
||||
|
||||
Vector3D span;
|
||||
span.Subtract(
|
||||
target->localOrigin.linearPosition,
|
||||
owner->localOrigin.linearPosition
|
||||
);
|
||||
rangeToTarget = span.Length();
|
||||
targetWithinRange = (rangeToTarget <= effectiveRange) ? True : False;
|
||||
return targetWithinRange;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// SendDamage Deliver this weapon's damage to the struck entity: build the
|
||||
// engine TakeDamageMessage (inflictor = our mech) and dispatch it AT the
|
||||
// target, whose handler routes damageZones[zone]->TakeDamage with the
|
||||
// authored per-type scales. INTERIM zone pick: a uniform roll over the
|
||||
// target's hull zones -- the authentic resolver is the reticle hit (aimed) or
|
||||
// the cylinder hit-location table (unaimed), both in the render/mech3 wave.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
MechWeapon::SendDamage(Entity *, Damage &)
|
||||
MechWeapon::SendDamage(Entity *target, Damage &damage)
|
||||
{
|
||||
Fail("MechWeapon::SendDamage -- mechweap.cpp not yet reconstructed");
|
||||
Check(this);
|
||||
Check(target);
|
||||
|
||||
int zone = -1;
|
||||
if (target->damageZoneCount > 0)
|
||||
{
|
||||
zone = Random(target->damageZoneCount);
|
||||
}
|
||||
|
||||
Entity::TakeDamageMessage
|
||||
hit(
|
||||
Entity::TakeDamageMessageID,
|
||||
sizeof(Entity::TakeDamageMessage),
|
||||
(owner != NULL) ? owner->GetEntityID() : EntityID::Null,
|
||||
zone,
|
||||
damage
|
||||
);
|
||||
target->Dispatch(&hit);
|
||||
|
||||
if (getenv("BT_MECH_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[dmg] '" << GetName()
|
||||
<< "' hit zone " << zone
|
||||
<< " amount=" << damage.damageAmount
|
||||
<< " type=" << damage.damageType;
|
||||
if (zone >= 0 && zone < target->damageZoneCount
|
||||
&& target->damageZones != NULL
|
||||
&& target->damageZones[zone] != NULL)
|
||||
{
|
||||
DEBUG_STREAM << " -> zoneLevel="
|
||||
<< target->damageZones[zone]->damageLevel;
|
||||
}
|
||||
DEBUG_STREAM << endl << flush;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -193,6 +193,17 @@
|
||||
ComputeOutputVoltage(); // vtable slot 17; the Emitter overrides
|
||||
// with its charge-voltage form
|
||||
|
||||
//
|
||||
// Targeting: the owner mech's target slot gates the Loaded->Firing
|
||||
// transition (no target = the denial blip, no shot); UpdateTargeting
|
||||
// (binary @004b9bdc) refreshes rangeToTarget / targetWithinRange from
|
||||
// the target's position each frame.
|
||||
//
|
||||
Logical
|
||||
HasActiveTarget();
|
||||
Logical
|
||||
UpdateTargeting();
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Local Data
|
||||
//
|
||||
|
||||
@@ -126,3 +126,27 @@ cycling — the NoAmmo roach-motel holds. Zero Fail.
|
||||
common block — the same silent-NULL trap as the Emitter::AttributeIndex
|
||||
crash. Real weapon handlers (fire/damage messages) come with the combat wave
|
||||
(grow the table in place).
|
||||
|
||||
## 5.3.16 — targeting + damage delivery
|
||||
|
||||
- `HasActiveTarget()` = owner's target slot non-NULL (binary owner+0x388);
|
||||
`UpdateTargeting()` (@004b9bdc) refreshes rangeToTarget/targetWithinRange
|
||||
from `target->localOrigin` every FSM tick (both weapon families call it
|
||||
right after the trigger sample). The Loaded→Firing gate is now the authentic
|
||||
`viewFireEnable && HasActiveTarget()` — a denied pull is the one-frame blip.
|
||||
- `SendDamage(target, damage)`: builds the engine 5-arg
|
||||
`Entity::TakeDamageMessage` (inflictor = our mech's EntityID) and Dispatches
|
||||
AT the target; the engine handler routes `damageZones[zone]->TakeDamage`.
|
||||
INTERIM zone pick = `Random(target->damageZoneCount)` — the authentic
|
||||
resolvers are the reticle aimed hit and the cylinder hit-location table
|
||||
(unaimed, zone −1 + the Mech TakeDamage override), both later waves.
|
||||
- Salvo semantics (BT411-verified): **burstCount is NOT in the zone armor
|
||||
formula** — one message = one amount×scale application. MissileLauncher
|
||||
therefore dispatches ONE message PER MISSILE (each carrying the ctor's
|
||||
salvo-split amount, each on its own random zone); a single split message
|
||||
would under-deliver the salvo by the round count. Interim all-rounds-hit
|
||||
until the Missile entity flight/seeker wave.
|
||||
- Live-verified (two-mech fight): PPC 11.77/hit (12×chargeRatio², type 4
|
||||
Energy), ER-M 3.43 (type 3 Laser), SRM6 5.83×6 (type 2 Explosive); repeat
|
||||
hits climb linearly; zones reach 1.0 Burning/Destroyed; no-target runs hold
|
||||
fire (0 FIRED across a 65s force-fire run without a target).
|
||||
|
||||
@@ -112,6 +112,25 @@ void
|
||||
AddPendingHeat(heatCostToFire);
|
||||
}
|
||||
|
||||
//
|
||||
// The salvo delivery: ONE TakeDamageMessage per missile, each carrying
|
||||
// the salvo-split per-missile damage (the ctor split the authored amount
|
||||
// across missileCount). The zone armor formula IGNORES burstCount
|
||||
// (BT411 combat-damage: one message = one amount*scale application), so
|
||||
// a single split message would under-deliver the salvo by the round
|
||||
// count. INTERIM instant-hit with every round hitting (its own random
|
||||
// zone) -- the Missile entity flight / seeker / proximity fuse joins the
|
||||
// entity-spawn wave.
|
||||
//
|
||||
if (targetWithinRange && owner != NULL
|
||||
&& owner->GetTargetEntity() != NULL)
|
||||
{
|
||||
for (int mm = 0; mm < missileCount; ++mm)
|
||||
{
|
||||
SendDamage(owner->GetTargetEntity(), damageData);
|
||||
}
|
||||
}
|
||||
|
||||
if (getenv("BT_MECH_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[fire] '" << GetName()
|
||||
|
||||
@@ -289,6 +289,8 @@ void
|
||||
//
|
||||
Logical trigger = CheckFireEdge();
|
||||
|
||||
targetWithinRange = UpdateTargeting();
|
||||
|
||||
//
|
||||
// Gate 1 (@4bbd36): weapon DESTROYED or its own sink at FailureHeat -> pin
|
||||
// full recoil + the unavailable alarm. No return -- the frame continues
|
||||
@@ -327,7 +329,11 @@ void
|
||||
case LoadedState:
|
||||
if (trigger)
|
||||
{
|
||||
if (viewFireEnable)
|
||||
//
|
||||
// The authentic fire gate (@4bbee6): armed in the current look
|
||||
// view AND a target under the reticle.
|
||||
//
|
||||
if (viewFireEnable && HasActiveTarget())
|
||||
{
|
||||
//
|
||||
// The ammo pull happens HERE, in the caller -- a failed pull
|
||||
|
||||
Reference in New Issue
Block a user