//===========================================================================// // File: projweap.cpp // // Project: BattleTech Brick: Mech weapons // // Contents: ProjectileWeapon -- ballistic (ammo-fed) weapon base // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(PROJWEAP_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(AMMOBIN_HPP) # include #endif #if !defined(RANDOM_HPP) # include #endif Derivation ProjectileWeapon::ClassDerivations( MechWeapon::ClassDerivations, "ProjectileWeapon" ); ProjectileWeapon::SharedData ProjectileWeapon::DefaultData( ProjectileWeapon::ClassDerivations, MechWeapon::MessageHandlers, MechWeapon::AttributeIndex, Subsystem::StateCount ); ProjectileWeapon::ProjectileWeapon( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data), ammoBinLink() { Check(owner); Check_Pointer(subsystem_resource); tracerInterval = subsystem_resource->tracerInterval; minTimeOfFlight = subsystem_resource->minTimeOfFlight; minJamChance = subsystem_resource->minJamChance; minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; tracerCounter = 0; ejectState = 0; totalTimeToEject = 0.0f; timeToEject = 0.0f; percentOfEject = 0.0f; tracerModelHandle = 0; leadPosition = Point3D(0.0f, 0.0f, 0.0f); launchVelocity = Vector3D(0.0f, 0.0f, 0.0f); tracerOrigin = Vector3D(0.0f, 0.0f, 0.0f); // // Link the AmmoBin subsystem the resource references by roster index (the // binary ctor resolves it from the owner's subsystem table and connects the // embedded link @0x43C). Shipped content always links a bin; log when the // data doesn't resolve one. // { Subsystem *roster_bin = NULL; if (subsystem_resource->ammoBinIndex >= 0 && subsystem_resource->ammoBinIndex < owner->GetSubsystemCount()) { roster_bin = owner->GetSubsystem(subsystem_resource->ammoBinIndex); } if (roster_bin != NULL) { ammoBinLink.Add(roster_bin); } if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[proj] '" << GetName() << "' ammoBinIndex=" << subsystem_resource->ammoBinIndex << " -> "; if (roster_bin != NULL) { DEBUG_STREAM << roster_bin->GetName() << " (rounds=" << ((AmmoBin *)roster_bin)->GetAmmoCount() << ")"; } else { DEBUG_STREAM << ""; } DEBUG_STREAM << endl << flush; } } // // Install the ballistic fire state machine (a replicant copy is driven by // console updates instead). // if (owner->GetInstance() != Entity::ReplicantInstance) { SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation); } Check_Fpu(); } ProjectileWeapon::~ProjectileWeapon() { } Logical ProjectileWeapon::TestClass(Mech &) { return True; } Logical ProjectileWeapon::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // FireWeapon The ballistic discharge (PARTIAL -- binary @004bc104). The // authentic body is heat + projectile/tracer spawn ONLY: the view/target gate, // the ammo pull and the recoil set all live in the CALLER (the Loaded case of // ProjectileWeaponSimulation) -- early-returning any gate from here while the // caller cycles the alarm anyway is exactly the 1995 "denied shot fakes a full // firing cycle" defect class. The projectile spawn (Missile entities / // tracer) needs the entity-spawn + targeting waves; MissileLauncher overrides // this for the salvo launch. //############################################################################# // void ProjectileWeapon::FireWeapon() { Check(this); // Ballistic heatCostToFire is stored 1e7-unit-NATIVE in the stream (SRM6 // reads 5.06e7 = +641K on its own sink -- the same design magnitude as the // PPC's +632K); dump it raw. (The EMITTER's authored value is small and // its 1e7 comes from the energy algebra -- see EMITTER.CPP.) AddPendingHeat(heatCostToFire); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[fire] '" << GetName() << "' FIRED (ballistic, recharge=" << rechargeRate << "s heat+=" << heatCostToFire << ")" << endl << flush; } } // //############################################################################# // CheckForJam -- the heat-scaled jam roll (binary @4bbfcc): // p = 0.41 * currentTemperature / failureTemperature, // clamped to [minJamChance, 1.0], rolled against a uniform random. // A jammed launcher clears only on ResetToInitialState (mission reset). // // TWO interim gates stand in for deferred systems (the BT411 port hit exactly // this): the authentic leading gate is LiveFireEnabled() -- the player // experience switch (novice mode = no jams), which needs the player-link // accessor wave -- and the binary's own `heatLoad <= 0` early-out is trivially // true once the heat model runs. Until LiveFireEnabled lands, gate the roll // on the sink actually being heat-degraded, so cold weapons fire reliably and // jams appear exactly when the heat economy says the weapon is cooking. //############################################################################# // Logical ProjectileWeapon::CheckForJam() { Check(this); if (GetHeatState() < DegradationHeat) // interim LiveFireEnabled stand-in { return False; } Scalar p = (0.41f * CurrentTemperatureOf()) / failureTemperature; if (minJamChance <= p) { if (p > 1.0f) { p = 1.0f; } } else { p = minJamChance; } return (p > (Scalar)Random) ? True : False; } // //############################################################################# // ProjectileWeaponSimulation The ballistic per-frame fire state machine // (binary @004bbd04, FULLY RECOVERED in the BT411 RE). The weapon state is // carried in the weapon alarm: 0/1/4/6 transient audio blips, 2 Loaded, // 3 Loading, 5 Jammed, 7 unavailable/NoAmmo (the roach-motel: nothing in the // machine ever leaves it -- only a mission reset does). // // PARTIAL: the leading PoweredSubsystemSimulation electrical step, the // destroyed / FailureHeat / mech-disabled hard gate (gate 1), the magazine // eject, the electrical-Ready recoil gate, the heat-scaled jam roll and the // HasActiveTarget half of the fire gate are deferred with the power / heat / // damage / targeting waves. What runs is authentic in shape: the single // trigger-edge sample, the dry-bin gate (gate 2), the Loaded ammo-pull -> // Firing -> Loading cycle with the denial blip, the Loading recoil bleed + // recharge dial, and the NoAmmo dry-fire blip. // // DEV hook BT_FORCE_FIRE=1: pulses the trigger whenever the weapon is Loaded // (same as the emitter hook) -- every armed weapon auto-fires at its authored // cadence until its bin runs dry. //############################################################################# // void ProjectileWeapon::ProjectileWeaponSimulation(Scalar time_slice) { Check(this); // // The PoweredSubsystem step first (binary @4bbd12): the HeatSink thermal // absorb/conduct + the electrical state machine. // PoweredSubsystem::PoweredSubsystemSimulation(time_slice); { static int forceFire = -1; if (forceFire < 0) { forceFire = (getenv("BT_FORCE_FIRE") != NULL) ? 1 : 0; } if (forceFire) { fireImpulse = (GetWeaponState() == LoadedState) ? 1.0f : 0.0f; } } // // The trigger edge is sampled ONCE, before the gates. // Logical trigger = CheckFireEdge(); // // Gate 1 (@4bbd36): weapon DESTROYED or its own sink at FailureHeat -> pin // full recoil + the unavailable alarm. No return -- the frame continues // into the state machine (whose NoAmmo case re-asserts). (The owning-mech- // disabled half joins with the damage wave.) // if (GetSimulationState() == 1 || GetHeatState() == FailureHeat) { if (getenv("BT_MECH_LOG") && GetWeaponState() != NoAmmoState) { DEBUG_STREAM << "[fire] '" << GetName() << "' -> UNAVAILABLE (gate1: heatState=" << GetHeatState() << " T=" << CurrentTemperatureOf() << ")" << endl << flush; } recoil = rechargeRate; weaponAlarm.SetLevel(NoAmmoState); } // // Gate 2: the linked AmmoBin. Empty (or missing) -> pin unavailable EVERY // frame while dry. // AmmoBin *bin = (AmmoBin *)ammoBinLink.Resolve(); if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoEmptyState) { if (getenv("BT_MECH_LOG") && GetWeaponState() != NoAmmoState) { DEBUG_STREAM << "[fire] '" << GetName() << "' -> NoAmmo (bin dry)" << endl << flush; } weaponAlarm.SetLevel(NoAmmoState); } switch (GetWeaponState()) { case LoadedState: if (trigger) { if (viewFireEnable) { // // The ammo pull happens HERE, in the caller -- a failed pull // just stays Loaded, silently. // if (bin != NULL && bin->FeedAmmo() != 0) { weaponAlarm.SetLevel(FiringState); ForceUpdate(); FireWeapon(); if (bin->GetAmmoState() == AmmoBin::AmmoEmptyState) { weaponAlarm.SetLevel(NoAmmoState); } else if (CheckForJam()) { weaponAlarm.SetLevel(JammedState); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[fire] '" << GetName() << "' JAMMED (T=" << CurrentTemperatureOf() << ")" << endl << flush; } } else { weaponAlarm.SetLevel(LoadingState); } ForceUpdate(); recoil = rechargeRate; } } else { // // THE DENIAL BLIP: a shot denied by the look-view gate does // SetLevel(4); SetLevel(2) -- a one-frame audio blip, the pip // stays Loaded and NO ammo is pulled. // weaponAlarm.SetLevel(TriggerDuringLoadState); weaponAlarm.SetLevel(LoadedState); } } break; case LoadingState: if (trigger) { weaponAlarm.SetLevel(TriggerDuringLoadState); weaponAlarm.SetLevel(LoadingState); } // // Recoil bleeds ONLY while the electrical supply is Ready (binary // @4bbdf5/@4bbe04); at zero (and a round chambered) -> Loaded. // if (GetVoltageState() == Ready) { recoil -= time_slice; if (recoil < 0.0f) { recoil = 0.0f; if (bin != NULL && bin->GetAmmoState() == AmmoBin::AmmoLoadedState) { weaponAlarm.SetLevel(LoadedState); if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[fire] '" << GetName() << "' LOADED (rounds=" << bin->GetAmmoCount() << " T=" << CurrentTemperatureOf() << ")" << endl << flush; } } } } ComputeOutputVoltage(); break; case JammedState: if (trigger) { weaponAlarm.SetLevel(TriggerDuringJamState); weaponAlarm.SetLevel(JammedState); } recoil = rechargeRate; break; case NoAmmoState: if (trigger) { weaponAlarm.SetLevel(DryTriggerState); // the dry-fire blip } weaponAlarm.SetLevel(NoAmmoState); // re-assert (the roach-motel) recoil = rechargeRate; ComputeOutputVoltage(); // dial pinned at 0 break; default: // 0/1/4/6: transient audio-blip states, no case body break; } Check_Fpu(); }