BT410 Phase 5.3.21: the muzzle wave -- rounds leave FROM THE GUN
MechWeapon::GetMuzzlePoint (binary @004b9948): the mount segment's world frame via GetSegmentToWorld (segmentToEntity x localToWorld), translation extracted by transforming the segment-space origin. The missile spawn now launches from the rack mount -- verified live: SRM6_1/SRM6_2 leave from distinct world positions 4.0 units above the hull origin, laterally offset per rack and rotated through the mech's yaw, on BOTH mechs. 19/19 launch-detonate over 100s, zero faults; smoke green. (The authored MuzzleVelocity up-tilt composed in the mount frame + the mech velocity term remain with the beam/render wave -- the seeker converges from the aimed initial velocity either way.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,394 +1,421 @@
|
||||
//===========================================================================//
|
||||
// File: mechweap.cpp //
|
||||
// Project: BattleTech Brick: Mech weapons //
|
||||
// Contents: MechWeapon -- the base weapon subsystem //
|
||||
//---------------------------------------------------------------------------//
|
||||
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
||||
// All Rights reserved worldwide //
|
||||
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
||||
//===========================================================================//
|
||||
|
||||
#include <bt.hpp>
|
||||
#pragma hdrstop
|
||||
|
||||
#if !defined(MECHWEAP_HPP)
|
||||
# include <mechweap.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(MECH_HPP)
|
||||
# include <mech.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(SEGMENT_HPP)
|
||||
# include <segment.hpp>
|
||||
#endif
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#if !defined(RANDOM_HPP)
|
||||
# include <random.hpp>
|
||||
#endif
|
||||
|
||||
Derivation
|
||||
MechWeapon::ClassDerivations(
|
||||
PoweredSubsystem::ClassDerivations,
|
||||
"MechWeapon"
|
||||
);
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Message handlers. MechWeapon publishes no handlers of its own yet (the
|
||||
// weapon fire/damage handler wave), but the set MUST be a real defined object:
|
||||
// the surviving CODE PPC.CPP binds the inherited name (`PPC::MessageHandlers`)
|
||||
// into PPC::DefaultData, and a declared-but-undefined static links as a
|
||||
// zero-filled common block -- the same silent-NULL trap as the
|
||||
// Emitter::AttributeIndex crash (see MECHWEAP.NOTES.md).
|
||||
//#############################################################################
|
||||
//
|
||||
MechWeapon::MessageHandlerSet
|
||||
MechWeapon::MessageHandlers(
|
||||
0,
|
||||
NULL,
|
||||
Subsystem::MessageHandlers
|
||||
);
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Attribute Support -- the 1995 binary table (@0x511890), IDs pinned so the
|
||||
// streamed per-mech control mappings resolve: TriggerState (0x13) is what the
|
||||
// authored fire buttons bind (the controls push then writes the trigger state
|
||||
// straight into fireImpulse each frame). The pads bridge our chain gap
|
||||
// 2..0x11 (parents publish nothing yet) -- REQUIRED, because Build leaves
|
||||
// uncovered gap slots uninitialized; they target rechargeLevel, harmless if a
|
||||
// stray mapping ever binds one (BT411 shipped the same). Pip*/WeaponRange
|
||||
// pads-to-rechargeLevel where the member isn't reconstructed yet are noted.
|
||||
//#############################################################################
|
||||
//
|
||||
#define MECHWEAP_PAD(n, name) \
|
||||
{ (int)MechWeapon::MechWeaponPadFirstAttributeID + n, name, \
|
||||
(Simulation::AttributePointer)&MechWeapon::rechargeLevel }
|
||||
|
||||
const MechWeapon::IndexEntry
|
||||
MechWeapon::AttributePointers[]=
|
||||
{
|
||||
MECHWEAP_PAD( 0, "MechWeaponPad02"),
|
||||
MECHWEAP_PAD( 1, "MechWeaponPad03"),
|
||||
MECHWEAP_PAD( 2, "MechWeaponPad04"),
|
||||
MECHWEAP_PAD( 3, "MechWeaponPad05"),
|
||||
MECHWEAP_PAD( 4, "MechWeaponPad06"),
|
||||
MECHWEAP_PAD( 5, "MechWeaponPad07"),
|
||||
MECHWEAP_PAD( 6, "MechWeaponPad08"),
|
||||
MECHWEAP_PAD( 7, "MechWeaponPad09"),
|
||||
MECHWEAP_PAD( 8, "MechWeaponPad0A"),
|
||||
MECHWEAP_PAD( 9, "MechWeaponPad0B"),
|
||||
MECHWEAP_PAD(10, "MechWeaponPad0C"),
|
||||
MECHWEAP_PAD(11, "MechWeaponPad0D"),
|
||||
MECHWEAP_PAD(12, "MechWeaponPad0E"),
|
||||
MECHWEAP_PAD(13, "MechWeaponPad0F"),
|
||||
MECHWEAP_PAD(14, "MechWeaponPad10"),
|
||||
MECHWEAP_PAD(15, "MechWeaponPad11"),
|
||||
ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel), // 0x12
|
||||
ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // 0x13
|
||||
ATTRIBUTE_ENTRY(MechWeapon, DistanceToTarget, rangeToTarget), // 0x14
|
||||
ATTRIBUTE_ENTRY(MechWeapon, TargetWithinRange, targetWithinRange), // 0x15
|
||||
ATTRIBUTE_ENTRY(MechWeapon, WeaponRange, effectiveRange), // 0x16
|
||||
{ (int)MechWeapon::PipPositionAttributeID, "PipPosition", // 0x17 (pip
|
||||
(Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // members
|
||||
{ (int)MechWeapon::PipColorAttributeID, "PipColor", // 0x18 not yet
|
||||
(Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // recon-
|
||||
{ (int)MechWeapon::PipExtendedRangeAttributeID, "PipExtendedRange", // 0x19 structed)
|
||||
(Simulation::AttributePointer)&MechWeapon::rechargeLevel },
|
||||
ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A
|
||||
ATTRIBUTE_ENTRY(MechWeapon, RearFiring, rearFiring), // 0x1B
|
||||
ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm) // 0x1C
|
||||
};
|
||||
|
||||
MechWeapon::AttributeIndexSet
|
||||
MechWeapon::AttributeIndex(
|
||||
ELEMENTS(MechWeapon::AttributePointers),
|
||||
MechWeapon::AttributePointers,
|
||||
Subsystem::AttributeIndex
|
||||
);
|
||||
|
||||
MechWeapon::SharedData
|
||||
MechWeapon::DefaultData(
|
||||
MechWeapon::ClassDerivations,
|
||||
MechWeapon::MessageHandlers,
|
||||
MechWeapon::AttributeIndex,
|
||||
Subsystem::StateCount
|
||||
);
|
||||
|
||||
MechWeapon::MechWeapon(
|
||||
Mech *owner,
|
||||
int subsystem_ID,
|
||||
SubsystemResource *subsystem_resource,
|
||||
SharedData &shared_data
|
||||
):
|
||||
PoweredSubsystem(owner, subsystem_ID, subsystem_resource, shared_data)
|
||||
{
|
||||
Check(owner);
|
||||
Check_Pointer(subsystem_resource);
|
||||
|
||||
rechargeRate = subsystem_resource->rechargeRate;
|
||||
weaponRange = subsystem_resource->weaponRange;
|
||||
damageAmount = subsystem_resource->damageAmount;
|
||||
damageType = subsystem_resource->damageType;
|
||||
heatCostToFire = subsystem_resource->heatCostToFire;
|
||||
explosionResourceID = subsystem_resource->explosionModelFile;
|
||||
targetWithinRange = False;
|
||||
|
||||
damageData.damageType = damageType;
|
||||
damageData.damageAmount = damageAmount;
|
||||
|
||||
//
|
||||
// Fire-machine state: spawn fully charged and Loaded (ready to fire).
|
||||
//
|
||||
fireImpulse = 0.0f;
|
||||
previousFireImpulse = 0.0f;
|
||||
rechargeLevel = 1.0f;
|
||||
rangeToTarget = 0.0f;
|
||||
effectiveRange = weaponRange;
|
||||
estimatedReadyTime = 0;
|
||||
recoil = 0.0f;
|
||||
weaponAlarm.Initialize(WeaponStateCount);
|
||||
weaponAlarm.SetLevel(LoadedState);
|
||||
|
||||
//
|
||||
// REAR-FIRING resolve (the binary ctor tail @004b99a8): a weapon is
|
||||
// rear-mounted when its MOUNT SEGMENT's site name carries the 'b' (back)
|
||||
// marker -- the back gun ports (sitelbgunport / siterbgunport) are the only
|
||||
// port names containing it. Rear-mounted weapons fire only into the
|
||||
// LOOK-BACK view; everything else fires forward. The look-state commit
|
||||
// (Mech::CommitLookState) re-arms viewFireEnable on every view change; a
|
||||
// weapon spawns armed for the forward view.
|
||||
//
|
||||
rearFiring = False;
|
||||
{
|
||||
EntitySegment *mount = owner->GetSegment(GetSegmentIndex());
|
||||
if (mount != NULL)
|
||||
{
|
||||
const char *mount_name = mount->GetName();
|
||||
if (mount_name != NULL && strstr(mount_name, "b") != NULL)
|
||||
{
|
||||
rearFiring = True;
|
||||
}
|
||||
}
|
||||
}
|
||||
viewFireEnable = (rearFiring == False);
|
||||
|
||||
if (getenv("BT_MECH_LOG"))
|
||||
{
|
||||
EntitySegment *mount = owner->GetSegment(GetSegmentIndex());
|
||||
DEBUG_STREAM << "[weap] '" << GetName()
|
||||
<< "' seg=" << GetSegmentIndex() << " mount=";
|
||||
if (mount != NULL)
|
||||
{
|
||||
DEBUG_STREAM << mount->GetName();
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "<none>";
|
||||
}
|
||||
DEBUG_STREAM << " rear=" << (int)rearFiring << endl << flush;
|
||||
}
|
||||
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
MechWeapon::~MechWeapon()
|
||||
{
|
||||
}
|
||||
|
||||
Logical
|
||||
MechWeapon::TestClass(Mech &)
|
||||
{
|
||||
return True;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// CheckFireEdge -- the rising-edge detector on fireImpulse (binary @004b9608).
|
||||
// TriggerState (attr 0x13) carries the raw ControlsButton INT bit-copied by the
|
||||
// streamed direct mapping (+(button+1) press / -(button+1) release), so the
|
||||
// release value reads as a negative NaN in float terms; the binary's x87
|
||||
// compare effectively treated it as "released". Comparing the BIT PATTERNS as
|
||||
// signed ints is sign-correct for the button ints AND for genuine float values
|
||||
// (0.0f = 0x0, positives > 0, negatives/NaNs < 0), reproducing the binary's
|
||||
// behavior exactly.
|
||||
//#############################################################################
|
||||
//
|
||||
Logical
|
||||
MechWeapon::CheckFireEdge()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
int current = *(int *)&fireImpulse;
|
||||
int previous = *(int *)&previousFireImpulse;
|
||||
Logical edge = (current > 0 && previous <= 0) ? True : False;
|
||||
previousFireImpulse = fireImpulse;
|
||||
return edge;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// ComputeOutputVoltage -- the recharge-dial writer (binary @004b9c9c, vtable
|
||||
// slot the Emitter overrides with its own voltage form):
|
||||
// rechargeLevel = (rechargeRate - recoil) / rechargeRate
|
||||
// The Loading tick decays recoil from rechargeRate toward 0, so the dial rises
|
||||
// 0 -> 1 over the authored RechargeRate seconds.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
MechWeapon::ComputeOutputVoltage()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
rechargeLevel = (rechargeRate - recoil) / rechargeRate;
|
||||
}
|
||||
|
||||
Logical
|
||||
MechWeapon::TestInstance() const
|
||||
{
|
||||
return IsDerivedFrom(ClassDerivations);
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// FireWeapon Abstract -- each concrete weapon (Emitter/PPC, ProjectileWeapon/
|
||||
// Gauss/MissileLauncher) overrides this. Reaching the base is a bug.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
MechWeapon::FireWeapon()
|
||||
{
|
||||
Fail("MechWeapon::FireWeapon -- should not be here (abstract)");
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// 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 *target, Damage &damage)
|
||||
{
|
||||
Check(this);
|
||||
Check(target);
|
||||
|
||||
int zone = -1;
|
||||
if (target->damageZoneCount > 0)
|
||||
{
|
||||
zone = Random(target->damageZoneCount);
|
||||
|
||||
//
|
||||
// DEV hook BT_FORCE_ZONE=n: pin every hit to zone n (concentrated
|
||||
// fire -- exercises the zone-death cascade headlessly).
|
||||
//
|
||||
{
|
||||
const char *force_zone = getenv("BT_FORCE_ZONE");
|
||||
if (force_zone != NULL)
|
||||
{
|
||||
int forced = atoi(force_zone);
|
||||
if (forced >= 0 && forced < target->damageZoneCount)
|
||||
{
|
||||
zone = forced;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// CreateStreamedSubsystem Model-load-time construction. Not yet reconstructed.
|
||||
//#############################################################################
|
||||
//
|
||||
int
|
||||
MechWeapon::CreateStreamedSubsystem(
|
||||
ResourceFile *,
|
||||
NotationFile *,
|
||||
const char *,
|
||||
const char *,
|
||||
SubsystemResource *,
|
||||
NotationFile *,
|
||||
const ResourceDirectories *,
|
||||
int
|
||||
)
|
||||
{
|
||||
Fail("MechWeapon::CreateStreamedSubsystem -- mechweap.cpp not yet reconstructed");
|
||||
return 0;
|
||||
}
|
||||
//===========================================================================//
|
||||
// File: mechweap.cpp //
|
||||
// Project: BattleTech Brick: Mech weapons //
|
||||
// Contents: MechWeapon -- the base weapon subsystem //
|
||||
//---------------------------------------------------------------------------//
|
||||
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
|
||||
// All Rights reserved worldwide //
|
||||
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
|
||||
//===========================================================================//
|
||||
|
||||
#include <bt.hpp>
|
||||
#pragma hdrstop
|
||||
|
||||
#if !defined(MECHWEAP_HPP)
|
||||
# include <mechweap.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(MECH_HPP)
|
||||
# include <mech.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(SEGMENT_HPP)
|
||||
# include <segment.hpp>
|
||||
#endif
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#if !defined(RANDOM_HPP)
|
||||
# include <random.hpp>
|
||||
#endif
|
||||
|
||||
Derivation
|
||||
MechWeapon::ClassDerivations(
|
||||
PoweredSubsystem::ClassDerivations,
|
||||
"MechWeapon"
|
||||
);
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Message handlers. MechWeapon publishes no handlers of its own yet (the
|
||||
// weapon fire/damage handler wave), but the set MUST be a real defined object:
|
||||
// the surviving CODE PPC.CPP binds the inherited name (`PPC::MessageHandlers`)
|
||||
// into PPC::DefaultData, and a declared-but-undefined static links as a
|
||||
// zero-filled common block -- the same silent-NULL trap as the
|
||||
// Emitter::AttributeIndex crash (see MECHWEAP.NOTES.md).
|
||||
//#############################################################################
|
||||
//
|
||||
MechWeapon::MessageHandlerSet
|
||||
MechWeapon::MessageHandlers(
|
||||
0,
|
||||
NULL,
|
||||
Subsystem::MessageHandlers
|
||||
);
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// Attribute Support -- the 1995 binary table (@0x511890), IDs pinned so the
|
||||
// streamed per-mech control mappings resolve: TriggerState (0x13) is what the
|
||||
// authored fire buttons bind (the controls push then writes the trigger state
|
||||
// straight into fireImpulse each frame). The pads bridge our chain gap
|
||||
// 2..0x11 (parents publish nothing yet) -- REQUIRED, because Build leaves
|
||||
// uncovered gap slots uninitialized; they target rechargeLevel, harmless if a
|
||||
// stray mapping ever binds one (BT411 shipped the same). Pip*/WeaponRange
|
||||
// pads-to-rechargeLevel where the member isn't reconstructed yet are noted.
|
||||
//#############################################################################
|
||||
//
|
||||
#define MECHWEAP_PAD(n, name) \
|
||||
{ (int)MechWeapon::MechWeaponPadFirstAttributeID + n, name, \
|
||||
(Simulation::AttributePointer)&MechWeapon::rechargeLevel }
|
||||
|
||||
const MechWeapon::IndexEntry
|
||||
MechWeapon::AttributePointers[]=
|
||||
{
|
||||
MECHWEAP_PAD( 0, "MechWeaponPad02"),
|
||||
MECHWEAP_PAD( 1, "MechWeaponPad03"),
|
||||
MECHWEAP_PAD( 2, "MechWeaponPad04"),
|
||||
MECHWEAP_PAD( 3, "MechWeaponPad05"),
|
||||
MECHWEAP_PAD( 4, "MechWeaponPad06"),
|
||||
MECHWEAP_PAD( 5, "MechWeaponPad07"),
|
||||
MECHWEAP_PAD( 6, "MechWeaponPad08"),
|
||||
MECHWEAP_PAD( 7, "MechWeaponPad09"),
|
||||
MECHWEAP_PAD( 8, "MechWeaponPad0A"),
|
||||
MECHWEAP_PAD( 9, "MechWeaponPad0B"),
|
||||
MECHWEAP_PAD(10, "MechWeaponPad0C"),
|
||||
MECHWEAP_PAD(11, "MechWeaponPad0D"),
|
||||
MECHWEAP_PAD(12, "MechWeaponPad0E"),
|
||||
MECHWEAP_PAD(13, "MechWeaponPad0F"),
|
||||
MECHWEAP_PAD(14, "MechWeaponPad10"),
|
||||
MECHWEAP_PAD(15, "MechWeaponPad11"),
|
||||
ATTRIBUTE_ENTRY(MechWeapon, PercentDone, rechargeLevel), // 0x12
|
||||
ATTRIBUTE_ENTRY(MechWeapon, TriggerState, fireImpulse), // 0x13
|
||||
ATTRIBUTE_ENTRY(MechWeapon, DistanceToTarget, rangeToTarget), // 0x14
|
||||
ATTRIBUTE_ENTRY(MechWeapon, TargetWithinRange, targetWithinRange), // 0x15
|
||||
ATTRIBUTE_ENTRY(MechWeapon, WeaponRange, effectiveRange), // 0x16
|
||||
{ (int)MechWeapon::PipPositionAttributeID, "PipPosition", // 0x17 (pip
|
||||
(Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // members
|
||||
{ (int)MechWeapon::PipColorAttributeID, "PipColor", // 0x18 not yet
|
||||
(Simulation::AttributePointer)&MechWeapon::rechargeLevel }, // recon-
|
||||
{ (int)MechWeapon::PipExtendedRangeAttributeID, "PipExtendedRange", // 0x19 structed)
|
||||
(Simulation::AttributePointer)&MechWeapon::rechargeLevel },
|
||||
ATTRIBUTE_ENTRY(MechWeapon, EstimatedReadyTime, estimatedReadyTime),// 0x1A
|
||||
ATTRIBUTE_ENTRY(MechWeapon, RearFiring, rearFiring), // 0x1B
|
||||
ATTRIBUTE_ENTRY(MechWeapon, WeaponState, weaponAlarm) // 0x1C
|
||||
};
|
||||
|
||||
MechWeapon::AttributeIndexSet
|
||||
MechWeapon::AttributeIndex(
|
||||
ELEMENTS(MechWeapon::AttributePointers),
|
||||
MechWeapon::AttributePointers,
|
||||
Subsystem::AttributeIndex
|
||||
);
|
||||
|
||||
MechWeapon::SharedData
|
||||
MechWeapon::DefaultData(
|
||||
MechWeapon::ClassDerivations,
|
||||
MechWeapon::MessageHandlers,
|
||||
MechWeapon::AttributeIndex,
|
||||
Subsystem::StateCount
|
||||
);
|
||||
|
||||
MechWeapon::MechWeapon(
|
||||
Mech *owner,
|
||||
int subsystem_ID,
|
||||
SubsystemResource *subsystem_resource,
|
||||
SharedData &shared_data
|
||||
):
|
||||
PoweredSubsystem(owner, subsystem_ID, subsystem_resource, shared_data)
|
||||
{
|
||||
Check(owner);
|
||||
Check_Pointer(subsystem_resource);
|
||||
|
||||
rechargeRate = subsystem_resource->rechargeRate;
|
||||
weaponRange = subsystem_resource->weaponRange;
|
||||
damageAmount = subsystem_resource->damageAmount;
|
||||
damageType = subsystem_resource->damageType;
|
||||
heatCostToFire = subsystem_resource->heatCostToFire;
|
||||
explosionResourceID = subsystem_resource->explosionModelFile;
|
||||
targetWithinRange = False;
|
||||
|
||||
damageData.damageType = damageType;
|
||||
damageData.damageAmount = damageAmount;
|
||||
|
||||
//
|
||||
// Fire-machine state: spawn fully charged and Loaded (ready to fire).
|
||||
//
|
||||
fireImpulse = 0.0f;
|
||||
previousFireImpulse = 0.0f;
|
||||
rechargeLevel = 1.0f;
|
||||
rangeToTarget = 0.0f;
|
||||
effectiveRange = weaponRange;
|
||||
estimatedReadyTime = 0;
|
||||
recoil = 0.0f;
|
||||
weaponAlarm.Initialize(WeaponStateCount);
|
||||
weaponAlarm.SetLevel(LoadedState);
|
||||
|
||||
//
|
||||
// REAR-FIRING resolve (the binary ctor tail @004b99a8): a weapon is
|
||||
// rear-mounted when its MOUNT SEGMENT's site name carries the 'b' (back)
|
||||
// marker -- the back gun ports (sitelbgunport / siterbgunport) are the only
|
||||
// port names containing it. Rear-mounted weapons fire only into the
|
||||
// LOOK-BACK view; everything else fires forward. The look-state commit
|
||||
// (Mech::CommitLookState) re-arms viewFireEnable on every view change; a
|
||||
// weapon spawns armed for the forward view.
|
||||
//
|
||||
rearFiring = False;
|
||||
{
|
||||
EntitySegment *mount = owner->GetSegment(GetSegmentIndex());
|
||||
if (mount != NULL)
|
||||
{
|
||||
const char *mount_name = mount->GetName();
|
||||
if (mount_name != NULL && strstr(mount_name, "b") != NULL)
|
||||
{
|
||||
rearFiring = True;
|
||||
}
|
||||
}
|
||||
}
|
||||
viewFireEnable = (rearFiring == False);
|
||||
|
||||
if (getenv("BT_MECH_LOG"))
|
||||
{
|
||||
EntitySegment *mount = owner->GetSegment(GetSegmentIndex());
|
||||
DEBUG_STREAM << "[weap] '" << GetName()
|
||||
<< "' seg=" << GetSegmentIndex() << " mount=";
|
||||
if (mount != NULL)
|
||||
{
|
||||
DEBUG_STREAM << mount->GetName();
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "<none>";
|
||||
}
|
||||
DEBUG_STREAM << " rear=" << (int)rearFiring << endl << flush;
|
||||
}
|
||||
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
MechWeapon::~MechWeapon()
|
||||
{
|
||||
}
|
||||
|
||||
Logical
|
||||
MechWeapon::TestClass(Mech &)
|
||||
{
|
||||
return True;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// CheckFireEdge -- the rising-edge detector on fireImpulse (binary @004b9608).
|
||||
// TriggerState (attr 0x13) carries the raw ControlsButton INT bit-copied by the
|
||||
// streamed direct mapping (+(button+1) press / -(button+1) release), so the
|
||||
// release value reads as a negative NaN in float terms; the binary's x87
|
||||
// compare effectively treated it as "released". Comparing the BIT PATTERNS as
|
||||
// signed ints is sign-correct for the button ints AND for genuine float values
|
||||
// (0.0f = 0x0, positives > 0, negatives/NaNs < 0), reproducing the binary's
|
||||
// behavior exactly.
|
||||
//#############################################################################
|
||||
//
|
||||
Logical
|
||||
MechWeapon::CheckFireEdge()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
int current = *(int *)&fireImpulse;
|
||||
int previous = *(int *)&previousFireImpulse;
|
||||
Logical edge = (current > 0 && previous <= 0) ? True : False;
|
||||
previousFireImpulse = fireImpulse;
|
||||
return edge;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// ComputeOutputVoltage -- the recharge-dial writer (binary @004b9c9c, vtable
|
||||
// slot the Emitter overrides with its own voltage form):
|
||||
// rechargeLevel = (rechargeRate - recoil) / rechargeRate
|
||||
// The Loading tick decays recoil from rechargeRate toward 0, so the dial rises
|
||||
// 0 -> 1 over the authored RechargeRate seconds.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
MechWeapon::ComputeOutputVoltage()
|
||||
{
|
||||
Check(this);
|
||||
|
||||
rechargeLevel = (rechargeRate - recoil) / rechargeRate;
|
||||
}
|
||||
|
||||
Logical
|
||||
MechWeapon::TestInstance() const
|
||||
{
|
||||
return IsDerivedFrom(ClassDerivations);
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// FireWeapon Abstract -- each concrete weapon (Emitter/PPC, ProjectileWeapon/
|
||||
// Gauss/MissileLauncher) overrides this. Reaching the base is a bug.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
MechWeapon::FireWeapon()
|
||||
{
|
||||
Fail("MechWeapon::FireWeapon -- should not be here (abstract)");
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// GetMuzzlePoint (binary @004b9948) -- the mount segment's world-frame
|
||||
// position: segmentToEntity x localToWorld (GetSegmentToWorld), translation
|
||||
// extracted by transforming the segment-space origin. This is what makes
|
||||
// fire leave FROM THE GUN and follow the torso twist.
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
MechWeapon::GetMuzzlePoint(Point3D &point)
|
||||
{
|
||||
Check(this);
|
||||
|
||||
point = owner->localOrigin.linearPosition;
|
||||
|
||||
if (GetSegmentIndex() >= 0)
|
||||
{
|
||||
EntitySegment *segment = owner->GetSegment(GetSegmentIndex());
|
||||
if (segment != NULL)
|
||||
{
|
||||
LinearMatrix to_world;
|
||||
owner->GetSegmentToWorld(*segment, &to_world);
|
||||
point.Multiply(Point3D(0.0f, 0.0f, 0.0f), to_world);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// 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 *target, Damage &damage)
|
||||
{
|
||||
Check(this);
|
||||
Check(target);
|
||||
|
||||
int zone = -1;
|
||||
if (target->damageZoneCount > 0)
|
||||
{
|
||||
zone = Random(target->damageZoneCount);
|
||||
|
||||
//
|
||||
// DEV hook BT_FORCE_ZONE=n: pin every hit to zone n (concentrated
|
||||
// fire -- exercises the zone-death cascade headlessly).
|
||||
//
|
||||
{
|
||||
const char *force_zone = getenv("BT_FORCE_ZONE");
|
||||
if (force_zone != NULL)
|
||||
{
|
||||
int forced = atoi(force_zone);
|
||||
if (forced >= 0 && forced < target->damageZoneCount)
|
||||
{
|
||||
zone = forced;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// CreateStreamedSubsystem Model-load-time construction. Not yet reconstructed.
|
||||
//#############################################################################
|
||||
//
|
||||
int
|
||||
MechWeapon::CreateStreamedSubsystem(
|
||||
ResourceFile *,
|
||||
NotationFile *,
|
||||
const char *,
|
||||
const char *,
|
||||
SubsystemResource *,
|
||||
NotationFile *,
|
||||
const ResourceDirectories *,
|
||||
int
|
||||
)
|
||||
{
|
||||
Fail("MechWeapon::CreateStreamedSubsystem -- mechweap.cpp not yet reconstructed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -204,6 +204,15 @@
|
||||
Logical
|
||||
UpdateTargeting();
|
||||
|
||||
//
|
||||
// The muzzle world position (binary @004b9948): the MOUNT segment's
|
||||
// world frame -- rounds and beams leave FROM THE GUN, following the
|
||||
// torso twist. Falls back to the mech origin when the mount
|
||||
// segment doesn't resolve.
|
||||
//
|
||||
void
|
||||
GetMuzzlePoint(Point3D &point);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Local Data
|
||||
//
|
||||
|
||||
@@ -382,7 +382,13 @@ void
|
||||
HostManager *host_manager = application->GetHostManager();
|
||||
Check(host_manager);
|
||||
|
||||
//
|
||||
// The round leaves FROM THE GUN: the mount segment's world
|
||||
// frame (GetMuzzlePoint @004b9948 -- follows the torso twist).
|
||||
// The rotation stays the mech's (the missile aims by velocity).
|
||||
//
|
||||
Origin muzzle = owner->localOrigin;
|
||||
GetMuzzlePoint(muzzle.linearPosition);
|
||||
//
|
||||
// NoCollisionVolumeFlag is LOAD-BEARING: collision volumes are
|
||||
// the Mover DEFAULT (IsCollisionVolume = the flag NOT set), and
|
||||
|
||||
Reference in New Issue
Block a user