BT410 Phase 5.3.15: the electrical charge model -- real charge, closed feedback loop
The emitter charge cycle is now the authentic electrical model end to end.
- WIRE-VERIFIED resource fix: the Emitter block is graphicLength /
dischargeTime / seekVoltage[5] (FRACTIONS of the generator's rated voltage,
-1 sentinel) / seekVoltageRecommendedIndex. The old two-field guess read
seekVoltage[3]=0.99 as "dischargeTime 0.99s" -- the true PPC discharge is
0.2s, and the calibration reads seekV={6000,7000,8000,9900} x rated 10000,
recommended gear 2: the documented curve exactly.
- Ctor calibration (@004bb120): energyTotal=(dmg+heat)x1e7; EC pins
E=0.5V^2EC to energyTotal at the recommended gear; voltageScale calibrates
the exponential charge to reach seekV[rec] in the authored RechargeRate
seconds cold; damageFraction = the damage share.
- TrackSeekVoltage (@004ba838) + ChargeTimeScale (@004b0d50): the charge
integrates from the generator through the heat-stretched time scale, and
the I2R loss heats the GENERATOR -- verified GeneratorA at 477.9K charging
its PPC (BT411 predicted ~+480K) while the avionics generator idles at 77.
Hot generators charge slower: the heat/firepower feedback loop is CLOSED.
- Emitter::ComputeOutputVoltage override (now virtual, as in the binary
vtable): dial = level/seekV[gear], 0.01 snap, over-1 clamp.
- Sub-stepped Loading (1/60s slices -- the BT411 weapon-brick fix) + the
documented-divergence overcharge rescue. VERIFIED: the PPC loads at level
~7920, inside the binary's exact [7920,8080] snap window.
- FireWeapon energy algebra (@004bace8): damage/heat = energy shares x
chargeRatio^2. VERIFIED dmg=11.78 / heat=1.079e8 at ratio 0.9906. The
heatCost x 1e7 partial is retired -- heat now comes from the energy.
Zero Fail; jams/shutdowns regressions stay live under spam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@
|
|||||||
# include <mech.hpp>
|
# include <mech.hpp>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
Derivation
|
Derivation
|
||||||
Emitter::ClassDerivations(
|
Emitter::ClassDerivations(
|
||||||
MechWeapon::ClassDerivations,
|
MechWeapon::ClassDerivations,
|
||||||
@@ -71,13 +73,105 @@ Emitter::Emitter(
|
|||||||
Check_Pointer(subsystem_resource);
|
Check_Pointer(subsystem_resource);
|
||||||
|
|
||||||
chargeLevel = 0.0f;
|
chargeLevel = 0.0f;
|
||||||
|
seekRate = 0.0f;
|
||||||
|
damagePortion = 0.0f;
|
||||||
|
heatPortion = 0.0f;
|
||||||
|
firingActive = 0;
|
||||||
|
|
||||||
|
graphicLength = subsystem_resource->graphicLength;
|
||||||
dischargeTime = subsystem_resource->dischargeTime;
|
dischargeTime = subsystem_resource->dischargeTime;
|
||||||
dischargeTimer = 0.0f;
|
dischargeTimer = dischargeTime;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Install the beam-weapon fire state machine (a replicant copy is driven by
|
// Bring-up-safe pre-init: the electrical block below overwrites these with
|
||||||
// console updates / ServiceDischarge instead, still staged).
|
// the calibrated values when the voltage source is live.
|
||||||
//
|
//
|
||||||
|
energyCoefficient = 1.0f;
|
||||||
|
seekVoltageIndex = 0;
|
||||||
|
seekVoltageRecommendedIndex = 0;
|
||||||
|
minSeekVoltageIndex = 0;
|
||||||
|
maxSeekVoltageIndex = 0;
|
||||||
|
{
|
||||||
|
for (int sv = 0; sv < 5; ++sv)
|
||||||
|
{
|
||||||
|
seekVoltage[sv] = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// energyTotal = (damage + heat) x 1e7 -- the discharge energy in the
|
||||||
|
// 1e7-native heat units.
|
||||||
|
//
|
||||||
|
energyTotal = (damageData.damageAmount + heatCostToFire) * 1.0e7f;
|
||||||
|
|
||||||
|
//
|
||||||
|
// The SeekVoltage calibration (binary ctor @004bb120): the authored curve
|
||||||
|
// values are FRACTIONS of the powering generator's rated voltage (-1
|
||||||
|
// sentinel ends the list); the energy coefficient pins E = 0.5*V^2*EC to
|
||||||
|
// energyTotal exactly at the recommended gear; and voltageScale calibrates
|
||||||
|
// the exponential charge level(t) = Vrated*(1 - exp(-t/(EC*voltageScale)))
|
||||||
|
// to reach the recommended seek voltage in the authored RechargeRate
|
||||||
|
// seconds on a COLD generator (0.0001 == 1/Vrated). Generator heat then
|
||||||
|
// stretches the scale through ChargeTimeScale -- the heat/firepower
|
||||||
|
// feedback.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
Generator *src = (Generator *)ResolveVoltageSource();
|
||||||
|
if (src != NULL)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
seekVoltage[i] = 0.0f;
|
||||||
|
}
|
||||||
|
for (i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
if (subsystem_resource->seekVoltage[i] == -1.0f)
|
||||||
|
{
|
||||||
|
maxSeekVoltageIndex = i - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
seekVoltage[i] = subsystem_resource->seekVoltage[i]
|
||||||
|
* src->RatedVoltageOf();
|
||||||
|
}
|
||||||
|
seekVoltageRecommendedIndex =
|
||||||
|
subsystem_resource->seekVoltageRecommendedIndex;
|
||||||
|
seekVoltageIndex = seekVoltageRecommendedIndex;
|
||||||
|
minSeekVoltageIndex = 0;
|
||||||
|
|
||||||
|
Scalar v = seekVoltage[seekVoltageRecommendedIndex];
|
||||||
|
energyCoefficient = energyTotal / (v * v * 0.5f);
|
||||||
|
voltageScale = (rechargeRate
|
||||||
|
/ -(Scalar)log((double)(1.0f - 0.0001f * v)))
|
||||||
|
/ energyCoefficient;
|
||||||
|
|
||||||
|
if (getenv("BT_POWER_LOG"))
|
||||||
|
{
|
||||||
|
DEBUG_STREAM << "[charge] '" << GetName()
|
||||||
|
<< "' seekV={" << seekVoltage[0] << "," << seekVoltage[1]
|
||||||
|
<< "," << seekVoltage[2] << "," << seekVoltage[3]
|
||||||
|
<< "," << seekVoltage[4] << "} rec=" << seekVoltageRecommendedIndex
|
||||||
|
<< " max=" << maxSeekVoltageIndex
|
||||||
|
<< " EC=" << energyCoefficient
|
||||||
|
<< " vScale=" << voltageScale
|
||||||
|
<< " discharge=" << dischargeTime
|
||||||
|
<< " E=" << energyTotal << endl << flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// damageFraction = the damage share of the discharge energy.
|
||||||
|
//
|
||||||
|
damageFraction = damageData.damageAmount /
|
||||||
|
(damageData.damageAmount + heatCostToFire);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Install the beam-weapon fire state machine; spawn LOADING (the charge
|
||||||
|
// integrates up from zero). (A replicant copy is driven by console
|
||||||
|
// updates / ServiceDischarge instead -- that path joins the network wave.)
|
||||||
|
//
|
||||||
|
weaponAlarm.SetLevel(LoadingState);
|
||||||
if (owner->GetInstance() != Entity::ReplicantInstance)
|
if (owner->GetInstance() != Entity::ReplicantInstance)
|
||||||
{
|
{
|
||||||
SetPerformance(&Emitter::EmitterSimulation);
|
SetPerformance(&Emitter::EmitterSimulation);
|
||||||
@@ -86,6 +180,80 @@ Emitter::Emitter(
|
|||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// TrackSeekVoltage -- integrate the charge from the powering generator
|
||||||
|
// (binary @004ba838): derive the seek rate from the voltage gap over the
|
||||||
|
// (heat-stretched) charge time-scale, step the level, and feed the charging
|
||||||
|
// I^2R loss back into the GENERATOR's heat -- recharging weapons is what
|
||||||
|
// heats generators, which conduct to their condensers and slow further
|
||||||
|
// charging through ChargeTimeScale. The heat/firepower feedback loop.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
void
|
||||||
|
Emitter::TrackSeekVoltage(Scalar time_slice)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
Generator *src = (Generator *)ResolveVoltageSource();
|
||||||
|
if (src == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scalar dtScale = ChargeTimeScale();
|
||||||
|
seekRate = (src->MeasuredVoltage() - chargeLevel) / dtScale;
|
||||||
|
chargeLevel = (seekRate / energyCoefficient) * time_slice + chargeLevel;
|
||||||
|
|
||||||
|
if (HeatModelActive())
|
||||||
|
{
|
||||||
|
src->AddPendingHeat(seekRate * seekRate * dtScale * time_slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// ComputeOutputVoltage -- the Emitter override (binary @004ba738): the
|
||||||
|
// recharge dial = the charge level as a fraction of the selected seek
|
||||||
|
// voltage, snapped to 1.0 within 0.01 and clamped to [0,1] (the byte-verified
|
||||||
|
// over-1 clamp zeroes the dial).
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
void
|
||||||
|
Emitter::ComputeOutputVoltage()
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
Scalar magnitude = (chargeLevel < 0.0f) ? -chargeLevel : chargeLevel;
|
||||||
|
if (magnitude > 1.0e-4f)
|
||||||
|
{
|
||||||
|
rechargeLevel = chargeLevel / seekVoltage[seekVoltageIndex];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rechargeLevel = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scalar snap = rechargeLevel - 1.0f;
|
||||||
|
if (snap < 0.0f)
|
||||||
|
{
|
||||||
|
snap = -snap;
|
||||||
|
}
|
||||||
|
if (snap <= 0.01f)
|
||||||
|
{
|
||||||
|
rechargeLevel = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rechargeLevel < 0.0f)
|
||||||
|
{
|
||||||
|
rechargeLevel = 0.0f;
|
||||||
|
}
|
||||||
|
else if (rechargeLevel > 1.0f)
|
||||||
|
{
|
||||||
|
rechargeLevel = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Emitter::~Emitter()
|
Emitter::~Emitter()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -121,32 +289,49 @@ void
|
|||||||
{
|
{
|
||||||
Check(this);
|
Check(this);
|
||||||
|
|
||||||
dischargeTimer = dischargeTime;
|
//
|
||||||
chargeLevel = 0.0f;
|
// Re-arm the beam-on countdown, then THE AUTHENTIC ENERGY ALGEBRA (binary
|
||||||
recoil = rechargeRate; // full recoil -> dial 0, decays in Loading
|
// @004bace8): the shot's damage and heat are the two shares of the stored
|
||||||
ComputeOutputVoltage();
|
// charge energy, scaled by the SQUARE of the charge ratio (the level as a
|
||||||
|
// fraction of the recommended seek voltage) -- an under-charged or
|
||||||
|
// down-geared shot delivers proportionally less of both. At full charge on
|
||||||
|
// the recommended gear: damage = the authored DamageAmount verbatim, heat =
|
||||||
|
// heatCostToFire x 1e7 (the 1e7-native chain).
|
||||||
|
//
|
||||||
|
dischargeTimer = dischargeTime;
|
||||||
|
|
||||||
|
Scalar vRec = seekVoltage[seekVoltageRecommendedIndex];
|
||||||
|
Scalar chargeRatio = (vRec > 0.0f) ? (chargeLevel / vRec) : 1.0f;
|
||||||
|
|
||||||
|
damagePortion = (damageFraction * energyTotal * 1.0e-7f)
|
||||||
|
* chargeRatio * chargeRatio;
|
||||||
|
heatPortion = (1.0f - damageFraction) * energyTotal
|
||||||
|
* chargeRatio * chargeRatio;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Dump the firing heat into our own thermal accumulator, under the
|
// The damage record carries this shot's portion (the submission at the
|
||||||
// heat-model experience gate (novice / standard fire generates no heat --
|
// owner's target joins the targeting/damage wave).
|
||||||
// authentic); the HeatSink step absorbs it next frame and conducts it
|
|
||||||
// toward the linked Condenser bank. The heat chain is 1e7-unit-native
|
|
||||||
// (the BT411 calibration audit): the authored EMITTER heatCostToFire is
|
|
||||||
// the closed form's full-charge value / 1e7 (PPC: 11 -> 1.1e8 units ->
|
|
||||||
// +632 K on its own 174000-mass sink). (PARTIAL: the
|
|
||||||
// (1-dF)*E*chargeRatio^2 charge-scaling joins the electrical-charge wave.)
|
|
||||||
//
|
//
|
||||||
|
damageData.damageAmount = damagePortion;
|
||||||
|
|
||||||
if (HeatModelActive())
|
if (HeatModelActive())
|
||||||
{
|
{
|
||||||
AddPendingHeat(heatCostToFire * 10000000.0f);
|
AddPendingHeat(heatPortion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Spend the charge.
|
||||||
|
//
|
||||||
|
ComputeOutputVoltage();
|
||||||
|
chargeLevel = 0.0f;
|
||||||
|
firingActive = 1;
|
||||||
|
|
||||||
if (getenv("BT_MECH_LOG"))
|
if (getenv("BT_MECH_LOG"))
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "[fire] '" << GetName()
|
DEBUG_STREAM << "[fire] '" << GetName()
|
||||||
<< "' FIRED (discharge=" << dischargeTime
|
<< "' FIRED (dmg=" << damagePortion
|
||||||
<< "s recharge=" << rechargeRate
|
<< " heat=" << heatPortion
|
||||||
<< "s heat+=" << heatCostToFire
|
<< " ratio=" << chargeRatio
|
||||||
<< " T=" << CurrentTemperatureOf() << ")" << endl << flush;
|
<< " T=" << CurrentTemperatureOf() << ")" << endl << flush;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,6 +347,7 @@ void
|
|||||||
{
|
{
|
||||||
Check(this);
|
Check(this);
|
||||||
|
|
||||||
|
firingActive = 0;
|
||||||
weaponAlarm.SetLevel(LoadingState);
|
weaponAlarm.SetLevel(LoadingState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,27 +453,55 @@ void
|
|||||||
weaponAlarm.SetLevel(LoadingState);
|
weaponAlarm.SetLevel(LoadingState);
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// Recharge only while the electrical supply is Ready (the authentic
|
// THE CHARGE INTEGRATION, sub-stepped at the pod's locked 60 fps: the
|
||||||
// charge integration gates on the voltage state): decay the recoil
|
// binary's Loading tick assumes fixed frames -- the Loaded transition
|
||||||
// over the authored RechargeRate seconds; the dial rises 0 -> 1.
|
// fires while the dial crosses the +-0.01 snap window around the
|
||||||
|
// selected seek voltage, a window a variable-dt spike can jump clean
|
||||||
|
// over (the level then overshoots, the over-1 clamp zeroes the dial,
|
||||||
|
// and the weapon bricks in Loading -- the BT411 weapon-brick fix).
|
||||||
|
// Charging gates on the electrical Ready state.
|
||||||
//
|
//
|
||||||
if (GetVoltageState() == Ready)
|
|
||||||
{
|
{
|
||||||
recoil -= time_slice;
|
const Scalar kPodFrame = 1.0f / 60.0f;
|
||||||
if (recoil <= 0.0f)
|
Scalar remaining = time_slice;
|
||||||
|
int guard = 600;
|
||||||
|
while (remaining > 0.0f && guard-- > 0
|
||||||
|
&& GetWeaponState() == LoadingState)
|
||||||
{
|
{
|
||||||
recoil = 0.0f;
|
Scalar slice = (remaining < kPodFrame) ? remaining : kPodFrame;
|
||||||
}
|
remaining -= slice;
|
||||||
}
|
if (GetVoltageState() == Ready)
|
||||||
ComputeOutputVoltage();
|
{
|
||||||
if (recoil == 0.0f)
|
TrackSeekVoltage(slice);
|
||||||
{
|
}
|
||||||
weaponAlarm.SetLevel(LoadedState);
|
ComputeOutputVoltage();
|
||||||
if (getenv("BT_MECH_LOG"))
|
if (rechargeLevel == 1.0f)
|
||||||
{
|
{
|
||||||
DEBUG_STREAM << "[fire] '" << GetName()
|
weaponAlarm.SetLevel(LoadedState);
|
||||||
<< "' LOADED (T=" << CurrentTemperatureOf() << ")"
|
if (getenv("BT_MECH_LOG"))
|
||||||
<< endl << flush;
|
{
|
||||||
|
DEBUG_STREAM << "[fire] '" << GetName()
|
||||||
|
<< "' LOADED (level=" << chargeLevel
|
||||||
|
<< " T=" << CurrentTemperatureOf() << ")"
|
||||||
|
<< endl << flush;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Overcharge rescue (a DOCUMENTED DIVERGENCE, BT411 issue #21:
|
||||||
|
// the binary deadlocks here -- a mid-charge gear change can
|
||||||
|
// land the level above the new gear's snap window, the over-1
|
||||||
|
// clamp zeroes the dial forever, and the weapon bricks. The
|
||||||
|
// arcade's own math says full == the gear's seek voltage, so
|
||||||
|
// an overcharged weapon IS loaded).
|
||||||
|
//
|
||||||
|
if (chargeLevel > seekVoltage[seekVoltageIndex]
|
||||||
|
&& seekVoltage[seekVoltageIndex] > 0.0f)
|
||||||
|
{
|
||||||
|
rechargeLevel = 1.0f;
|
||||||
|
weaponAlarm.SetLevel(LoadedState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -26,11 +26,19 @@
|
|||||||
//####################### Emitter Model Resource ########################
|
//####################### Emitter Model Resource ########################
|
||||||
//###########################################################################
|
//###########################################################################
|
||||||
|
|
||||||
|
//
|
||||||
|
// WIRE-VERIFIED layout: graphicLength, dischargeTime, then the SeekVoltage
|
||||||
|
// curve -- five FRACTIONS of the generator's rated voltage (-1 sentinel
|
||||||
|
// ends the list) -- and the recommended (default) gear index. (The earlier
|
||||||
|
// two-field guess read seekVoltage[3] = 0.99 as "dischargeTime 0.99 s".)
|
||||||
|
//
|
||||||
struct Emitter__SubsystemResource:
|
struct Emitter__SubsystemResource:
|
||||||
public MechWeapon::SubsystemResource
|
public MechWeapon::SubsystemResource
|
||||||
{
|
{
|
||||||
Scalar seekVoltage[5];
|
Scalar graphicLength;
|
||||||
Scalar dischargeTime;
|
Scalar dischargeTime;
|
||||||
|
Scalar seekVoltage[5];
|
||||||
|
int seekVoltageRecommendedIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
//###########################################################################
|
//###########################################################################
|
||||||
@@ -104,7 +112,10 @@
|
|||||||
FireWeapon();
|
FireWeapon();
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Per-frame simulation (the beam-weapon fire state machine).
|
// Per-frame simulation (the beam-weapon fire state machine) and the
|
||||||
|
// electrical charge model: the Loading state integrates currentLevel
|
||||||
|
// toward the powering generator's voltage (TrackSeekVoltage); Loaded snaps
|
||||||
|
// when the recharge dial reaches 1.0 (level ~= the selected seek voltage).
|
||||||
//
|
//
|
||||||
public:
|
public:
|
||||||
typedef void
|
typedef void
|
||||||
@@ -120,14 +131,32 @@
|
|||||||
EmitterSimulation(Scalar time_slice);
|
EmitterSimulation(Scalar time_slice);
|
||||||
void
|
void
|
||||||
ResetFiringState();
|
ResetFiringState();
|
||||||
|
void
|
||||||
|
TrackSeekVoltage(Scalar time_slice);
|
||||||
|
virtual void
|
||||||
|
ComputeOutputVoltage(); // the charge-voltage form (level/seekV)
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Local Data
|
// Local Data. chargeLevel is the ChargeLevel attribute alias of the raw
|
||||||
|
// charge voltage (currentLevel in the binary's naming).
|
||||||
//
|
//
|
||||||
protected:
|
protected:
|
||||||
Scalar chargeLevel;
|
Scalar chargeLevel; // the raw charge voltage (binary currentLevel @0x414)
|
||||||
Scalar dischargeTime;
|
Scalar dischargeTime;
|
||||||
Scalar dischargeTimer;
|
Scalar dischargeTimer;
|
||||||
|
Scalar graphicLength;
|
||||||
|
Scalar seekRate;
|
||||||
|
Scalar energyCoefficient;
|
||||||
|
Scalar energyTotal;
|
||||||
|
Scalar damageFraction;
|
||||||
|
Scalar damagePortion;
|
||||||
|
Scalar heatPortion;
|
||||||
|
int firingActive;
|
||||||
|
int seekVoltageIndex;
|
||||||
|
int seekVoltageRecommendedIndex;
|
||||||
|
int minSeekVoltageIndex;
|
||||||
|
int maxSeekVoltageIndex;
|
||||||
|
Scalar seekVoltage[5];
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -253,6 +253,8 @@
|
|||||||
GetHeatState() { Check(this); return heatAlarm.GetLevel(); }
|
GetHeatState() { Check(this); return heatAlarm.GetLevel(); }
|
||||||
Scalar
|
Scalar
|
||||||
CurrentTemperatureOf() { Check(this); return currentTemperature; }
|
CurrentTemperatureOf() { Check(this); return currentTemperature; }
|
||||||
|
Scalar
|
||||||
|
StartingTemperatureOf() { Check(this); return startingTemperature; }
|
||||||
void
|
void
|
||||||
AddPendingHeat(Scalar heat)
|
AddPendingHeat(Scalar heat)
|
||||||
{ Check(this); pendingHeat += heat; }
|
{ Check(this); pendingHeat += heat; }
|
||||||
|
|||||||
@@ -223,3 +223,44 @@ past ambient -- actively radiating the mech's heat away.
|
|||||||
(0.05 x 6 x 20 = 6 -- the authentic tank), coolant refilled to it.
|
(0.05 x 6 x 20 = 6 -- the authentic tank), coolant refilled to it.
|
||||||
|
|
||||||
Zero Fail; the full expert economy regression stays green.
|
Zero Fail; the full expert economy regression stays green.
|
||||||
|
|
||||||
|
## The electrical charge model (Phase 5.3.15, 2026-07-24)
|
||||||
|
|
||||||
|
The emitter charge cycle is REAL (all bodies authentic from the BT411 RE):
|
||||||
|
|
||||||
|
- **Resource fix (wire-verified)**: the Emitter block is graphicLength,
|
||||||
|
dischargeTime, seekVoltage[5] (FRACTIONS of the generator's rated voltage,
|
||||||
|
-1 sentinel), seekVoltageRecommendedIndex. The earlier two-field guess had
|
||||||
|
read seekVoltage[3] = 0.99 as "dischargeTime 0.99 s" -- the TRUE PPC
|
||||||
|
discharge is 0.2 s, and the calibration reads seekV = {6000, 7000, 8000,
|
||||||
|
9900} x rated 10000, rec gear 2, exactly the documented curve.
|
||||||
|
- **Ctor calibration** (@004bb120): energyTotal = (damage + heat) x 1e7 (PPC
|
||||||
|
2.3e8); EC pins E = 0.5 V^2 EC to energyTotal at the recommended gear;
|
||||||
|
voltageScale calibrates the exponential charge to reach seekV[rec] in the
|
||||||
|
authored RechargeRate seconds on a COLD generator; damageFraction = the
|
||||||
|
damage share.
|
||||||
|
- **TrackSeekVoltage** (@004ba838): seekRate = (genV - level)/ChargeTimeScale;
|
||||||
|
level += rate/EC x dt; and the charging I^2R loss lands in the GENERATOR's
|
||||||
|
pendingHeat -- recharging weapons is what heats generators. VERIFIED:
|
||||||
|
GeneratorA at 477.9 K charging its PPC (BT411 predicted ~+480K), GeneratorD
|
||||||
|
(avionics only) idle at 77.
|
||||||
|
- **ChargeTimeScale** (@004b0d50, PoweredSubsystem): the base voltageScale
|
||||||
|
stretched by (1 + thermalResistivity x generator temp rise) -- hot
|
||||||
|
generators charge slower. The feedback loop is CLOSED.
|
||||||
|
- **Emitter::ComputeOutputVoltage** override (@004ba738, now virtual as in the
|
||||||
|
binary vtable): dial = level/seekV[gear], snap to 1.0 within 0.01, over-1
|
||||||
|
clamp zeroes.
|
||||||
|
- **Sub-stepped Loading** (the BT411 weapon-brick fix): the charge integrates
|
||||||
|
in 1/60 s slices so the +-0.01 Loaded snap window can't be jumped by a dt
|
||||||
|
spike; plus the DOCUMENTED-DIVERGENCE overcharge rescue (the binary
|
||||||
|
deadlocks a weapon whose level lands above a re-geared snap window).
|
||||||
|
VERIFIED: the PPC loads at level ~7920 -- inside the exact [7920, 8080]
|
||||||
|
window from the binary's disasm.
|
||||||
|
- **FireWeapon energy algebra** (@004bace8): damage/heat = the two
|
||||||
|
energy shares x chargeRatio^2 (level/seekV[rec]). VERIFIED: dmg=11.78,
|
||||||
|
heat=1.079e8 at ratio 0.9906 -- ~98% of authored at the snap edge. The
|
||||||
|
heatCost x 1e7 partial is retired; the heat now comes from the ENERGY.
|
||||||
|
|
||||||
|
Deferred: the ToggleSeekVoltage gear button (needs the energy-weapon message
|
||||||
|
table ids 4..0xb with pads), ServiceDischarge (replicant beam path), the beam
|
||||||
|
build + damage submission (targeting/render waves).
|
||||||
|
|||||||
@@ -189,8 +189,9 @@
|
|||||||
GetWeaponState() { Check(this); return weaponAlarm.GetLevel(); }
|
GetWeaponState() { Check(this); return weaponAlarm.GetLevel(); }
|
||||||
Logical
|
Logical
|
||||||
CheckFireEdge();
|
CheckFireEdge();
|
||||||
void
|
virtual void
|
||||||
ComputeOutputVoltage();
|
ComputeOutputVoltage(); // vtable slot 17; the Emitter overrides
|
||||||
|
// with its charge-voltage form
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Local Data
|
// Local Data
|
||||||
|
|||||||
@@ -179,6 +179,39 @@ int
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// ChargeTimeScale -- the heat/firepower feedback (binary @004b0d50):
|
||||||
|
// rise = max(0, sourceTemperature - sourceStartingTemperature)
|
||||||
|
// scale = max(voltageScale,
|
||||||
|
// (thermalResistivityCoefficient * rise + 1) * voltageScale)
|
||||||
|
// A hot generator stretches the exponential charge constant, so recharging
|
||||||
|
// slows exactly when the electrical plant is cooking.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
Scalar
|
||||||
|
PoweredSubsystem::ChargeTimeScale()
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
Generator *source = (Generator *)voltageSource.Resolve();
|
||||||
|
if (source == NULL)
|
||||||
|
{
|
||||||
|
return voltageScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scalar rise = source->CurrentTemperatureOf()
|
||||||
|
- source->StartingTemperatureOf();
|
||||||
|
if (rise < 0.0f)
|
||||||
|
{
|
||||||
|
rise = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scalar stretched =
|
||||||
|
(thermalResistivityCoefficient * rise + 1.0f) * voltageScale;
|
||||||
|
return (stretched > voltageScale) ? stretched : voltageScale;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// PoweredSubsystemSimulation -- the per-frame electrical step (binary
|
// PoweredSubsystemSimulation -- the per-frame electrical step (binary
|
||||||
|
|||||||
@@ -93,6 +93,15 @@
|
|||||||
unsigned
|
unsigned
|
||||||
GetVoltageState() { Check(this); return electricalStateAlarm.GetLevel(); }
|
GetVoltageState() { Check(this); return electricalStateAlarm.GetLevel(); }
|
||||||
|
|
||||||
|
//
|
||||||
|
// The charge time-scale (binary @004b0d50) -- the heat/firepower
|
||||||
|
// feedback: the base voltageScale (the ctor-calibrated exponential
|
||||||
|
// charge constant) stretched by the powering generator's temperature
|
||||||
|
// rise above its start point.
|
||||||
|
//
|
||||||
|
Scalar
|
||||||
|
ChargeTimeScale();
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Per-frame simulation (heat step + the electrical state machine).
|
// Per-frame simulation (heat step + the electrical state machine).
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user