TestCatalogGlobal existed only to give the test host one [CultGlobal] routed to the catalog. VerseGrammar is the
game's first, so the test-only type is deleted and every fixture authors a VerseGrammar instead (63 call sites; the
missing-global tests assert on aetheria.verse_grammar). Stale comments about the test type are rewritten.
Builds (not yet run when committed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
StatValidation.PowerRequestFields registered only each consumer's own
top-level request field (Thruster.EnergyUsage, Shield.Capacity, and so on),
on the premise that PowerRequest reads exactly one PerformanceStat. That was
false for five of the eight consumers: Radiator.PowerRequest also reads
PumpedHeat and WasteHeat (its own early-out gate), AetherDrive.PowerRequest
also reads Torque, LambdaMultiplier, MaximumRpm and PassiveCoupling (its
whole spin-up arithmetic), Shield.PowerRequest (via RefreshReserve) also
reads RefillDuration/RestoreDuration, InstantWeapon.PowerRequest (via
RefreshInputCapacitor) also reads Cooldown/Count, and Sensor.PowerRequest
also reads PingCooldown. Any of those could carry a PowerSupply term and sail
straight past ValidateNoPowerSupplyOnRequest, corrupting the very request it
feeds -- exactly Soul's reproduced "OK Disperser" oscillation
(SOUL_RadiatorRequestDependsOnItsOwnGrantAndOscillates).
Audited directly against each PowerRequest method's own body (and the
RefreshReserve/RefreshInputCapacitor helpers it calls into), not guessed from
the behaviour's public surface.
This correctly refuses the real catalog now: `GameData/Aetheria.cc` carries
exactly six violations --
cold like my heart RadiatorData.PumpedHeat
Arctica RadiatorData.PumpedHeat
Skiron RadiatorData.PumpedHeat
OK Disperser RadiatorData.PumpedHeat
Iapyx RadiatorData.PumpedHeat
Traction AetherDriveData.Torque
-- re-authoring them (moving the PowerSupply term off the now-covered field,
or redesigning Radiator/AetherDrive's own PowerRequest to read a
nominal/undegraded value instead) is catalog and design work for Self/roles
authoring, not done here.
BrownoutTests.cs's own Radiator/AetherDrive brownout fixtures hit the same
wall -- they authored a PowerSupply term on PumpedHeat/Torque to test Cut 7's
"curve on the performance stat," which is now correctly refused since both
behaviours' PowerRequest reads that same stat. Neither behaviour has an
untouched field left to curve, so there is currently no valid catalog shape
that gives them a working brownout curve without also feeding it back into
their own request -- a real, scoped conflict between Cut 6 and Cut 7 for
these two consumers. The six affected tests are marked Skip with a header
comment explaining the conflict rather than silently reworked or deleted;
Thruster and ConstantWeapon are unaffected (their own PowerRequest never
reads the separate field their tests curve).
Pinned in tests/Aetheria.Shared.Tests/PowerCurveTests.cs:
UpsertRefusesARadiatorWhosePumpedHeatCarriesAPowerSupplyTerm and
UpsertRefusesAShieldWhoseRefillDurationCarriesAPowerSupplyTerm -- both fail
under the reverted (top-level-field-only) registry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PerformanceStat.Evaluate folded PowerSupply into the same Min/Max lerp factor
as Heat/Durability/Quality, so zero supply bottomed out at Min instead of 0
(measured: a radiator at zero power still pumped 19-25% of full; three of Cut
7's eight authored terms had Min == Max and were completely inert, since
lerp(x, x, anything) == x).
PowerSupply terms are now accumulated into their own multiplier and applied to
the fully-interpolated, fully-modified value instead of to the interpolation
factor. The term still lives in PerformanceStat.Terms -- it is still authored
per stat and still censused/validated the same way -- it is just no longer
blended into `factor`. Corrected the comments that stated the old premise
(Radiator.cs, Entity.cs, ItemData.cs's IStatContext.PowerSupplyFactor doc).
Pinned in tests/Aetheria.Shared.Tests/PowerCurveTests.cs:
APowerSupplyTermedStatWithNonzeroMinResolvesToExactlyZeroAtZeroSupply and
AMinEqualsMaxStatStillRespondsToItsPowerSupplyTerm -- both fail under the
reverted (lerp-based) mutation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven tests: a PowerSupply-termed stat degrades by its own authored exponent when the
grant drops and reads unchanged at full grant (the second read is deliberately taken after
a real cached value exists, so the missing-invalidate mutation is actually reachable rather
than hidden behind a fixture that only ever reads once); a stat with no PowerSupply term
never asks the context for one, cache or no cache; a power-termed stat recomputes at most
once per tick, not once per read; a thruster-style EnergyUsage field carrying a PowerSupply
term directly is refused at AetheriaStores.Open and at Upsert, naming the item and the
field; and a modifier whose magnitude carries a PowerSupply term is refused at equip when
its target is a known power-request stat, naming both items.
123 tests total (116 baseline + 7). tests/mutation_tests_stats_power_cut6.py: 10 mutations
(1 no-op control, 9 rule-killing), all behave as declared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>