//===========================================================================// // File: heat.cpp // // Project: BattleTech Brick: Mech subsystems // // Contents: HeatableSubsystem -- a MechSubsystem with a thermal state // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(HEAT_HPP) # include #endif #if !defined(MECH_HPP) # include #endif #if !defined(BTPLAYER_HPP) # include #endif #include // //############################################################################# // Tuning constants (byte-verified against the shipped image in the BT411 RE: // .data / literal-pool reads). //############################################################################# // static const Scalar HeatLoadScale = 0.002f; // heat-load normalising scale -> band [0,1] static const Scalar HeatLoadMinimum = 0.0f; static const Scalar HeatLoadMaximum = 1.0f; static const Scalar HeatEqualizeEpsilon = 1.0e-4f; // BalanceCoolant no-op band static const Scalar CoolantDrawGate = 1.0e-4f; // DrawCoolant |amount| gate static const Scalar CoolantDrawFloor = 0.0025f; // draw zero-floor + ACTIVE-off hysteresis static const Scalar CoolantActiveOn = 0.003f; // coolantActive ON threshold // //############################################################################# // Shared data support -- reuses the base Subsystem sets (no boot-critical // handlers / attributes of its own). //############################################################################# // Derivation HeatableSubsystem::ClassDerivations( MechSubsystem::ClassDerivations, "HeatableSubsystem" ); HeatableSubsystem::SharedData HeatableSubsystem::DefaultData( HeatableSubsystem::ClassDerivations, Subsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); // //############################################################################# //############################################################################# // HeatableSubsystem::HeatableSubsystem( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): MechSubsystem( owner, subsystem_ID, (MechSubsystem::SubsystemResource *)subsystem_resource, shared_data ) { Check(owner); Check_Pointer(subsystem_resource); ResetToInitialState(); Check_Fpu(); } // //############################################################################# //############################################################################# // HeatableSubsystem::~HeatableSubsystem() { } // //############################################################################# //############################################################################# // void HeatableSubsystem::ResetToInitialState() { Check(this); currentTemperature = 300.0f; heatLoad = 0.0f; } // //############################################################################# //############################################################################# // Logical HeatableSubsystem::TestClass(Mech &) { return True; } Logical HeatableSubsystem::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // CreateStreamedSubsystem Model-load-time construction (thermal resource + // damage-zone stream). Not yet reconstructed (see MECHSUB.NOTES.md). //############################################################################# // int HeatableSubsystem::CreateStreamedSubsystem( NotationFile *, const char *, const char *, SubsystemResource *, NotationFile *, const ResourceDirectories *, int ) { Fail("HeatableSubsystem::CreateStreamedSubsystem -- heat.cpp not yet reconstructed"); return 0; } //########################################################################### //############################## HeatSink ############################### //########################################################################### // //############################################################################# // Shared data support //############################################################################# // Derivation HeatSink::ClassDerivations( HeatableSubsystem::ClassDerivations, "HeatSink" ); // // The cockpit cooling toggle (id 3 -- the first subsystem-family message). // const HeatSink::HandlerEntry HeatSink::MessageHandlerEntries[]= { MESSAGE_ENTRY(HeatSink, ToggleCooling) }; HeatSink::MessageHandlerSet HeatSink::MessageHandlers( ELEMENTS(HeatSink::MessageHandlerEntries), HeatSink::MessageHandlerEntries, Subsystem::MessageHandlers ); HeatSink::SharedData HeatSink::DefaultData( HeatSink::ClassDerivations, HeatSink::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); // //############################################################################# // The heat sink -- a thermal mass with a coolant loop. A master sink drives // the per-frame thermal simulation. //############################################################################# // HeatSink::HeatSink( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): HeatableSubsystem(owner, subsystem_ID, subsystem_resource, shared_data), linkedSinks(), heatAlarm(3) { Check(owner); Check_Pointer(subsystem_resource); currentTemperature = subsystem_resource->startingTemperature; degradationTemperature = subsystem_resource->degradationTemperature; failureTemperature = subsystem_resource->failureTemperature; heatLoad = 0.0f; coolantEfficiency = 0.5f; thermalCapacity = 1.0f; coolantLevel = thermalCapacity; coolantDraw = 0.0f; coolantAvailable = 1; coolantActive = 0; startingTemperature = currentTemperature; thermalConductance = subsystem_resource->thermalConductance; heatFilter.SetSize(15, 0.0f); filterDecay = 0.4f; thermalMass = subsystem_resource->thermalMass; heatEnergy = thermalMass * startingTemperature; coolantFlowScale = 1.0f; massScale = 1.0f; pendingHeat = 0.0f; radiatedHeat = 0.0f; // // Wire the heat-conduction link: the resource names the roster slot of the // sink this one drains into (weapons/equipment -> the Condenser bank, the // Condensers -> the central HeatSink). The shipped stream orders sinks so // the target is already constructed; an unresolvable index leaves the sink // standalone (its own thermal mass only). // { Subsystem *linked = NULL; if (subsystem_resource->linkedSinkIndex >= 0 && subsystem_resource->linkedSinkIndex < owner->GetSubsystemCount()) { linked = owner->GetSubsystem(subsystem_resource->linkedSinkIndex); } if (linked != NULL) { linkedSinks.Add(linked); } if (getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[heat] '" << GetName() << "' linkedSinkIdx=" << subsystem_resource->linkedSinkIndex << " -> "; if (linked != NULL) { DEBUG_STREAM << linked->GetName(); } else { DEBUG_STREAM << ""; } DEBUG_STREAM << " thermalMass=" << thermalMass << " T0=" << currentTemperature << " degrade=" << degradationTemperature << " fail=" << failureTemperature << " conduct=" << thermalConductance << endl << flush; } } // // Install the per-frame thermal Performance (replicant copies are driven by // console updates instead). Derived classes (PoweredSubsystem, Generator, // the weapons) override with their own Performance in their ctors, each of // which chains this step. // if (owner->GetInstance() != Entity::ReplicantInstance) { SetPerformance(&HeatSink::HeatSinkSimulation); } Check_Fpu(); } // //############################################################################# //############################################################################# // HeatSink::~HeatSink() { } // //############################################################################# //############################################################################# // void HeatSink::ResetToInitialState(Logical /*powered*/) { Check(this); currentTemperature = startingTemperature; heatLoad = 0.0f; coolantLevel = thermalCapacity; coolantDraw = 0.0f; coolantActive = 0; heatEnergy = thermalMass * startingTemperature; pendingHeat = 0.0f; radiatedHeat = 0.0f; heatAlarm.SetLevel(0); } // //############################################################################# //############################################################################# // Logical HeatSink::TestClass(Mech &) { return True; } Logical HeatSink::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // HeatModelActive -- the heat-model master switch (binary FUN_004ad7d4): // owner mech -> Entity::playerLink -> BTPlayer::heatModelOn, ON only for // veteran / expert experience. Standard mode has NO heat consequences -- // authentic, not a gap. A NULL player link (unlinked dev mech / target dummy) // reads ON, matching the permissive dev-rig behavior; the binary derefs // unguarded (a linked player always exists in pod missions). //############################################################################# // Logical HeatSink::HeatModelActive() { Check(this); if (owner == NULL) { return True; } BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); if (player == NULL) { return True; } return player->IsHeatModelOn(); } // //############################################################################# // HeatSinkSimulation -- the per-frame thermal step (binary @004ad924). // Under the heat-model experience gate: absorb the pending heat into the // thermal mass, recompute the temperature and the smoothed heat-load reading, // conduct into the linked sink, and run the coolant draw. The degradation / // failure alarm drive is OUTSIDE the gate (authentic -- with the model off the // temperature never moves, so the alarm just holds Normal). //############################################################################# // void HeatSink::HeatSinkSimulation(Scalar time_slice) { Check(this); if (HeatModelActive()) { heatEnergy += pendingHeat; currentTemperature = heatEnergy / thermalMass; UpdateHeatLoad(); pendingHeat = 0.0f; ConductHeat(time_slice); } if (HeatModelActive()) { UpdateCoolant(time_slice); } // // Drive the degradation / failure alarm. // if (currentTemperature > failureTemperature) { heatAlarm.SetLevel(FailureHeat); } else if (currentTemperature > degradationTemperature) { heatAlarm.SetLevel(DegradationHeat); } else { heatAlarm.SetLevel(NormalHeat); } // // BT_HEAT_LOG: a 5-second per-sink census (temperature / coolant / load). // if (getenv("BT_HEAT_LOG")) { static Scalar censusAccum = 0.0f; censusAccum += time_slice; if (censusAccum >= 5.0f) { censusAccum = 0.0f; DEBUG_STREAM << "[heat-t] " << GetName() << " T=" << currentTemperature << " cool=" << coolantLevel << "/" << thermalCapacity << " load=" << heatLoad << endl << flush; } } Check_Fpu(); } // //############################################################################# // UpdateHeatLoad -- recompute the radiated heat and feed the normalised sample // through the 15-sample running-average filter to produce the smoothed // heatLoad reading in the [0,1] band (binary @004ad7f0; the shaping constants // are byte-verified from the image). //############################################################################# // void HeatSink::UpdateHeatLoad() { Check(this); radiatedHeat = currentTemperature * coolantLevel; Scalar sample = HeatLoadScale * radiatedHeat; if (sample < HeatLoadMinimum) { sample = HeatLoadMinimum; } else if (sample > HeatLoadMaximum) { sample = HeatLoadMaximum; } heatFilter.Add(sample); heatLoad = heatFilter.CalculateAverage(); } // //############################################################################# // ConductHeat -- conduct heat into the linked sink, then rebalance coolant so // the hotter side sheds load (binary @004ad8ac). //############################################################################# // void HeatSink::ConductHeat(Scalar time_slice) { Check(this); HeatSink *other = (HeatSink *)linkedSinks.Resolve(); if (other != NULL && coolantAvailable != 0) { Scalar flow = ComputeHeatFlow(other, time_slice); other->pendingHeat += flow; pendingHeat -= flow; BalanceCoolant(time_slice); } } // //############################################################################# // BalanceCoolant -- move coolant between this sink and its linked sink so that // the hotter side sheds load (binary @004ada94). Clamped on both ends so // neither sink goes below empty or above its capacity. //############################################################################# // void HeatSink::BalanceCoolant(Scalar time_slice) { Check(this); HeatSink *other = (HeatSink *)linkedSinks.Resolve(); if (other == NULL) { return; } Scalar spread = radiatedHeat - other->radiatedHeat; if (spread < 0.0f) { spread = -spread; } if (spread <= HeatEqualizeEpsilon) { return; } Scalar delta = other->radiatedHeat / currentTemperature - coolantLevel; // // Clamp delta to +/- (thermalCapacity * dt). // Scalar limit = thermalCapacity * time_slice; if (delta < -limit) { delta = -limit; } else if (delta > limit) { delta = limit; } // // Clamp so this sink stays within [0, thermalCapacity]. // Scalar hi = thermalCapacity - coolantLevel; Scalar lo = -coolantLevel; if (delta < lo) delta = lo; else if (delta > hi) delta = hi; // // Clamp so the other sink stays within its own [0, thermalCapacity]. // Scalar otherLo = -(other->thermalCapacity - other->coolantLevel); if (delta < otherLo) delta = otherLo; else if (delta > other->coolantLevel) delta = other->coolantLevel; delta = coolantFlowScale * delta; coolantLevel += delta; other->coolantLevel -= delta; } // //############################################################################# // UpdateCoolant -- consume coolant proportional to the current load, request a // top-up from the central cooling system, and update the draw state machine // (binary @004adbf8). The draw scales with THIS subsystem's own structural // damage: an undamaged subsystem leaks nothing (the coolant bars stay full on // a pristine mech); the draw rises only as the sink itself takes battle // damage. //############################################################################# // void HeatSink::UpdateCoolant(Scalar time_slice) { Check(this); coolantDraw = GetSubsystemDamageLevel() * heatLoad; if (coolantDraw < CoolantDrawFloor) { coolantDraw = 0.0f; } Scalar amount = coolantDraw * time_slice; if (coolantLevel < amount) { amount = coolantLevel; } coolantLevel -= amount; if (amount > CoolantDrawGate || amount < -CoolantDrawGate) { coolantLevel += DrawCoolant(amount); } // // The draw state machine (hysteresis: ON above 0.003, OFF below 0.0025). // if (coolantActive == 0 && coolantDraw > CoolantActiveOn) { coolantActive = 1; } else if (coolantActive == 1 && coolantDraw < CoolantDrawFloor) { coolantActive = 0; } } // //############################################################################# // ComputeHeatFlow -- conductive heat exchange between this sink and 'other' // (binary @004ad9ec): // tau = thermalMass / massScale // denom = tau + other->thermalMass // q = (currentTemperature*massScale // - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom) // * tau // * (1 - exp( -dt * thermalConductance // * (coolantLevel / thermalCapacity) // * coolantFlowScale / denom )) //############################################################################# // Scalar HeatSink::ComputeHeatFlow(HeatSink *other, Scalar time_slice) { Check(this); Check(other); Scalar tau = thermalMass / massScale; Scalar denom = tau + other->thermalMass; Scalar equilibrium = currentTemperature * massScale - (other->heatEnergy + other->pendingHeat + heatEnergy) / denom; Scalar response = 1.0f - (Scalar)exp( -time_slice * thermalConductance * (coolantLevel / thermalCapacity) * coolantFlowScale / denom ); return equilibrium * tau * response; } // //############################################################################# // DrawCoolant -- ask the central cooling system for coolant and return how // much was actually supplied. Base sinks supply nothing on their own; the // Reservoir (the coolant store) overrides this as the source. //############################################################################# // Scalar HeatSink::DrawCoolant(Scalar) { Check(this); return 0.0f; } // //############################################################################# // ToggleCoolingMessageHandler -- the cockpit cooling on/off switch (binary // @004ad6f8, id 3): novice-locked, press-only; toggles the coolant supply and // the conduction flow together (OFF = this sink stops conducting into the // bank -- its heat strands until cooling is switched back on). //############################################################################# // void HeatSink::ToggleCoolingMessageHandler(ReceiverDataMessageOf *message) { Check(this); Check(message); if (NoviceLockout()) { return; } if (message->dataContents <= 0) // press only { return; } if (coolantAvailable != 0) { coolantAvailable = 0; coolantFlowScale = 0.0f; } else { coolantAvailable = 1; coolantFlowScale = 1.0f; } if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[cool] '" << GetName() << "' cooling toggled -> " << coolantAvailable << endl << flush; } } //########################################################################### //############################# HeatWatcher ############################# //########################################################################### Derivation HeatWatcher::ClassDerivations( MechSubsystem::ClassDerivations, "HeatWatcher" ); HeatWatcher::SharedData HeatWatcher::DefaultData( HeatWatcher::ClassDerivations, Subsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); HeatWatcher::HeatWatcher( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): MechSubsystem( owner, subsystem_ID, (MechSubsystem::SubsystemResource *)subsystem_resource, shared_data ), watchedLink(), heatAlarm(3) { Check(owner); Check_Pointer(subsystem_resource); degradationTemperature = subsystem_resource->degradationTemperature; failureTemperature = subsystem_resource->failureTemperature; watchedSubsystem = subsystem_resource->watchedSubsystem; // // The master instance runs WatchSimulation per-frame; the install is // deferred with that (staged) method. // Check_Fpu(); } HeatWatcher::~HeatWatcher() { } Logical HeatWatcher::TestClass(Mech &) { return True; } Logical HeatWatcher::TestInstance() const { return IsDerivedFrom(ClassDerivations); } void HeatWatcher::ResetToInitialState(Logical /*powered*/) { Check(this); heatAlarm.SetLevel(0); } // // Per-frame: resolve the watched subsystem, read its temperature, drive the // 3-level alarm. Not yet reconstructed. // void HeatWatcher::WatchSimulation(Scalar) { Fail("HeatWatcher::WatchSimulation -- heat.cpp not yet reconstructed"); } //########################################################################### //############################## Condenser ############################# //########################################################################### Derivation Condenser::ClassDerivations( HeatSink::ClassDerivations, "Condenser" ); // // The cockpit condenser-valve button (the binary's Condenser handler table // has exactly ONE entry: id 4, "MoveValve"). ToggleCooling (id 3) is // inherited from the HeatSink chain. // const Condenser::HandlerEntry Condenser::MessageHandlerEntries[]= { MESSAGE_ENTRY(Condenser, MoveValve) }; Condenser::MessageHandlerSet Condenser::MessageHandlers( ELEMENTS(Condenser::MessageHandlerEntries), Condenser::MessageHandlerEntries, HeatSink::MessageHandlers ); Condenser::SharedData Condenser::DefaultData( Condenser::ClassDerivations, Condenser::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); Condenser::Condenser( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): HeatSink(owner, subsystem_ID, subsystem_resource, shared_data) { Check(owner); Check_Pointer(subsystem_resource); // // The authentic ctor (binary @4ae568): the valve opens at 1, the // refrigeration output rides the inherited massScale slot, and the // condenser's own coolantFlowScale starts 0 -- the valve recompute // (RecomputeValves: MoveValve presses, and once at mech spawn) assigns // each condenser its share of the total valve opening. // valveState = 1; coolantFlowScale = 0.0f; refrigerationFactor = subsystem_resource->refrigerationFactor; massScale = refrigerationFactor; // // Condenser number from the segment-name DIGIT suffix ("Condenser1" -> 1; // the letter form -0x40 is the GENERATOR convention). // const char *name = GetName(); condenserNumber = name[strlen(name) - 1] - '0'; if (getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[cond] '" << GetName() << "' refrigFactor=" << refrigerationFactor << " #" << condenserNumber << endl << flush; } // // Install the per-frame refrigeration Performance. // if (owner->GetInstance() != Entity::ReplicantInstance) { SetPerformance(&Condenser::RefrigerationSimulation); } Check_Fpu(); } Condenser::~Condenser() { } Logical Condenser::TestClass(Mech &) { return True; } Logical Condenser::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // RefrigerationSimulation -- the condenser's per-frame step (binary @4ae4d8): // recompute the refrigeration output as (1 - own structural damage) * // refrigerationFactor, clamped >= 1, into the inherited massScale slot -- an // undamaged condenser behaves refrigerationFactor-times "hotter" in the // conduction exchange, actively pumping heat toward the central bank and // chilling itself below ambient -- then run the base HeatSink step. //############################################################################# // void Condenser::RefrigerationSimulation(Scalar time_slice) { Check(this); massScale = (1.0f - GetSubsystemDamageLevel()) * refrigerationFactor; if (massScale < 1.0f) { massScale = 1.0f; } HeatSink::HeatSinkSimulation(time_slice); } // //############################################################################# // MoveValveMessageHandler -- the cockpit condenser-valve button (binary // @4ae464, id 4): novice-locked, press-only; cycles THIS condenser's valve // 1 -> 5 -> 50 -> 0 -> 1, then recomputes every condenser's flow share. //############################################################################# // void Condenser::MoveValveMessageHandler(ReceiverDataMessageOf *message) { Check(this); Check(message); if (NoviceLockout()) { return; } if (message->dataContents <= 0) // press only { return; } switch (valveState) { case 0: valveState = 1; break; case 1: valveState = 5; break; case 5: valveState = 50; break; case 50: valveState = 0; break; default: return; // unknown -> no change, no recompute } RecomputeValves((Entity *)owner); } // //############################################################################# // RecomputeValves -- assign every condenser its coolant flow share // (valve / sum-of-valves) after a valve move (binary @0049f788). All valves // at the spawn default (1) yield equal shares. (The condenser-alarm change // pulse joins with the gauge wave.) //############################################################################# // void Condenser::RecomputeValves(Entity *owner_mech) { Check(owner_mech); int count = owner_mech->GetSubsystemCount(); int total = 0; int i; for (i = 2; i < count; ++i) { Subsystem *s = owner_mech->GetSubsystem(i); if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations)) { total += ((Condenser *)s)->valveState; } } for (i = 2; i < count; ++i) { Subsystem *s = owner_mech->GetSubsystem(i); if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations)) { Condenser *condenser = (Condenser *)s; condenser->coolantFlowScale = (total > 0) ? ((Scalar)condenser->valveState / (Scalar)total) : 0.0f; if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[valve] '" << condenser->GetName() << "' valve=" << condenser->valveState << " flow=" << condenser->coolantFlowScale << endl; } } } if (getenv("BT_MECH_LOG")) { DEBUG_STREAM << flush; } } //########################################################################### //######################### AggregateHeatSink ########################### //########################################################################### Derivation AggregateHeatSink::ClassDerivations( HeatSink::ClassDerivations, "HeatSinkBank" ); AggregateHeatSink::SharedData AggregateHeatSink::DefaultData( AggregateHeatSink::ClassDerivations, HeatSink::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); // //############################################################################# // The heat-sink bank (binary ctor @4ae8d0): the aggregate count scales the // conductance (0.1 x count, byte-verified), the ambient setpoint defaults // 300 K (the mission's [mission] temperature overwrites it in the PlayerLink // pass -- that override joins the Mech-PlayerLink wave), and a master runs // the RADIATOR Performance instead of the base heat step. //############################################################################# // AggregateHeatSink::AggregateHeatSink( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): HeatSink(owner, subsystem_ID, subsystem_resource, shared_data) { Check(owner); Check_Pointer(subsystem_resource); heatSinkCount = subsystem_resource->heatSinkCount; ambientTemperature = 300.0f; thermalConductance = 0.1f * (Scalar)heatSinkCount * thermalConductance; if (getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[bank] '" << GetName() << "' heatSinkCount=" << heatSinkCount << " conductance=" << thermalConductance << endl << flush; } if (owner->GetInstance() != Entity::ReplicantInstance) { SetPerformance(&AggregateHeatSink::RadiatorSimulation); } Check_Fpu(); } AggregateHeatSink::~AggregateHeatSink() { } Logical AggregateHeatSink::TestClass(Mech &) { return True; } Logical AggregateHeatSink::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // RadiatorSimulation -- the bank's per-frame step (binary @4ae73c), replacing // the base HeatSinkSimulation: under the heat-model gate, absorb + recompute // the load, then the AMBIENT RADIATOR -- relax the bank toward the setpoint // target (300 - (300 - ambient) x 3) with rate k = conductance x (1 - own // damage) x (coolant/capacity) x flowScale / mass. This is the system's ONLY // heat exit. Tail: top the bank's coolant up from the attached store via the // DrawCoolant virtual (the reservoir-attach routing joins that wave; the base // supplies 0 until then, a harmless no-op). //############################################################################# // void AggregateHeatSink::RadiatorSimulation(Scalar time_slice) { Check(this); if (HeatModelActive()) { heatEnergy += pendingHeat; currentTemperature = heatEnergy / thermalMass; UpdateHeatLoad(); pendingHeat = 0.0f; Scalar target = 300.0f - (300.0f - ambientTemperature) * 3.0f; Scalar decay = 1.0f - (Scalar)exp( (double)(-(time_slice * thermalConductance * (1.0f - GetSubsystemDamageLevel()) * (coolantLevel / thermalCapacity) * coolantFlowScale) / thermalMass) ); Scalar shed = -((currentTemperature * massScale - target) * thermalMass * decay); pendingHeat += shed; if (getenv("BT_HEAT_LOG")) { static Scalar bankAccum = 0.0f; bankAccum += time_slice; if (bankAccum >= 5.0f) { bankAccum = 0.0f; DEBUG_STREAM << "[heat-t] " << GetName() << " (bank)" << " T=" << currentTemperature << " shed=" << shed << " cool=" << coolantLevel << "/" << thermalCapacity << " load=" << heatLoad << endl << flush; } } } // // Coolant top-up from the attached store. // Scalar deficit = thermalCapacity - coolantLevel; if (deficit > 1.0e-4f) { coolantLevel += DrawCoolant(deficit); } }