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:
Cyd
2026-07-24 10:21:43 -05:00
co-authored by Claude Fable 5
parent b772e716fe
commit 76cfc64353
13 changed files with 686 additions and 10 deletions
+56
View File
@@ -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.
+18 -3
View File
@@ -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();
+61 -1
View File
@@ -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
+13 -1
View File
@@ -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
+15
View File
@@ -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.
+281
View File
@@ -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);
}
+83
View File
@@ -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.
+87 -4
View File
@@ -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;
}
}
//
+11
View File
@@ -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).
+19
View File
@@ -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()
+7 -1
View File
@@ -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