Resolve request/brownout conflict: evaluate power requests nominally

A power request may not depend on power supply (Cut 6/F6), and continuous
consumers brown out through a power term on their own performance stats
(Cut 7). For Radiator (PumpedHeat) and AetherDrive (Torque) those were the
same stat, making six shipped catalog records illegal and leaving six
BrownoutTests cases skipped.

Resolved per the operator's ruling: a power request is evaluated
nominally -- what the item wants at full supply, not what it is currently
managing. EquippedItem.EvaluateNominalPower (mirroring ConditionRatio's
own NominalContext) pins only PowerSupplyFactor to 1; Heat/Durability
stay live, since they are not the term a request would be circular
through. Every PowerRequest/RefreshReserve/RefreshInputCapacitor
implementation now reads a StatValidation.PowerRequestFields stat through
it, so the same stat can carry a brownout curve and be a request field
without the request depending on its own answer.

StatValidation.ValidateNoPowerSupplyOnRequest (F6's static half of the
rule, forbidding the term outright) is deleted -- nominal evaluation
removes the circularity at the root instead. Only the dynamic
modifier-chain half (StatModifier.ValidateNoPowerSupplyChain) still
refuses anything, since EvaluateNominalPower forwards ScaleModifier/
ConstantModifier unchanged (real, not nominal).

Un-skips BrownoutTests' six Radiator/AetherDrive cases and adds two new
regression tests (a direct EvaluateNominalPower probe, and a two-tick
Radiator demand check) backed by a new mutation harness. Updates the four
PowerCurveTests/AetheriaStoresTests cases that pinned the now-superseded
static refusal to instead assert acceptance. GameData/Aetheria.cc is
unchanged and still validates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-19 06:58:57 +02:00
commit 8bbf280756
19 changed files with 528 additions and 209 deletions

View file

@ -882,19 +882,23 @@ public static class Program
// a partial grant read exactly like a full one instead of a reduced one. This authors the term, once, on the
// one performance stat each of the four curve-eligible behaviours (EnergyDraw has none -- see EnergyDraw.cs)
// actually reads for its continuous effect: ThrusterData.Thrust, AetherDriveData.Torque,
// RadiatorData.PumpedHeat, ConstantWeaponData.Damage. None of the four is a power-request field
// (StatValidation.PowerRequestFields), so authoring the term here does not trip
// ValidateNoPowerSupplyOnRequest. Exponent 1 (linear) is the gentle default the ruling calls for -- half
// supply reads as half performance, not a cliff -- and is left for the operator to steepen per design later.
// Dry run unless passed "apply"; every record must derive cleanly or nothing is written, same contract as
// ShieldMigrate.
// RadiatorData.PumpedHeat, ConstantWeaponData.Damage. Two of the four (AetherDriveData.Torque,
// RadiatorData.PumpedHeat) are ALSO power-request fields (StatValidation.PowerRequestFields) -- at the time
// this tool authored the shipped catalog's curves that briefly made those two records illegal under F6's
// now-deleted static check (StatValidation.ValidateNoPowerSupplyOnRequest); the nominal-request ruling
// (docs/stats-and-power-cut.md, operator ruling 2026-09-19, see ItemData.cs's PowerRequestFields comment)
// resolved that by having PowerRequest read request fields nominally instead of forbidding the term, so
// authoring it here on a request field is legal again. Exponent 1 (linear) is the gentle default the ruling
// calls for -- half supply reads as half performance, not a cliff -- and is left for the operator to steepen
// per design later. Dry run unless passed "apply"; every record must derive cleanly or nothing is written,
// same contract as ShieldMigrate.
private static int BrownoutMigrate(bool apply)
{
var db = AetherDb.Open(catalogWritable: apply);
const float gentleExponent = 1f;
// (behaviour type, target performance-stat field, short label) -- deliberately NOT the request field
// (StatValidation.PowerRequestFields already forbids a PowerSupply term there for each of these types).
// (behaviour type, target performance-stat field, short label) -- two of these are also request fields
// (StatValidation.PowerRequestFields); that is legal under the nominal-request ruling above.
var targets = new (Type BehaviorType, string Field, string Label)[]
{
(typeof(ThrusterData), nameof(ThrusterData.Thrust), "thrust"),