Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using static CultMath.math;
|
|
|
|
|
|
|
|
|
|
// Cut 3 (docs/stats-and-power-cut.md §1.2): the sole owner of every power grant for one Entity, stepped once per
|
|
|
|
|
// tick from Entity.Update, before any equipped item's Behaviors execute. It replaces
|
|
|
|
|
// Entity.TryConsumeEnergy/CanConsumeEnergy: at HEAD every draw succeeded as long as one reactor was online, and
|
|
|
|
|
// any shortfall was silently taxed onto the reactor as heat instead of ever being refused (§0.5). The bus makes
|
|
|
|
|
// the ceiling real: demand beyond what this tick's generation and stored capacitor charge can cover goes unmet.
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
//
|
|
|
|
|
// Cut 5 (docs/stats-and-power-cut.md §1.3) replaces the single shared fraction with priority tiers
|
|
|
|
|
// (PowerTiers.cs): the total this tick's supply can cover (TotalGrant, unchanged by tiers -- see below) is
|
|
|
|
|
// walked tier by tier, lowest number first. A tier is fed in full before the next tier sees anything left; a
|
|
|
|
|
// tier with no demand simply passes its whole share down; within a tier, every consumer's own request is
|
|
|
|
|
// rationed by the same fraction, so two requests of unequal size still divide proportionally. This is the death
|
|
|
|
|
// of the old "whoever equipped first drains the capacitors first" hidden priority (§0.5), replaced with an
|
|
|
|
|
// authored one instead of no priority at all.
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
//
|
|
|
|
|
// Bus capacitors are charged and drained only here (§0b): Reactor no longer touches them, and neither does any
|
Cut 4: input capacitors close the last direct capacitor spend
Deletes Entity.CanSpendCapacitorCharge/TrySpendCapacitorCharge (and the
now-dead _capacitors bookkeeping list that only fed them) -- Cut 3's
named, temporary exception for the four instant draws. InstantWeapon
(both fire sites), Sensor.Ping, and Shield.CanTakeHit/TakeHit now spend
from their own InputCapacitor, filled continuously by PowerBus like
every other IPowerConsumer instead of touching the entity's shared bus
capacitors directly.
InputCapacitor's Capacity/Rate derive from the owning item's Energy and
Cooldown, with an authored-override hook (ChargedWeapon repurposes its
previously-unused ChargeEnergy stat as that override, per the map's own
instruction, rather than adding a new field). A weapon/sensor fire
attempt only ever spends exactly Capacity, so it only ever succeeds at
full charge -- no fractional shot. Shield is the map's named exception:
its reserve is continuous, not an activation buffer, so it allows a
partial draw; its size (no Energy/Cooldown pair exists on Shield) is a
flagged judgment call, not a spec'd formula.
Adds InputCapacitorTests.cs (8 tests) and
mutation_tests_stats_power_cut4.py (7 mutations, all behave as
declared); removes the one PowerBusTests.cs test and Cut 3 mutation
case that exercised the now-deleted Entity methods.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:52:15 +02:00
|
|
|
// IPowerConsumer. The four instant draws the cut map names (a burst, a shot, a ping, a hit taken) are
|
|
|
|
|
// IPowerConsumers too as of Cut 4: each spends from its own InputCapacitor (InstantWeapon, Sensor, Shield),
|
|
|
|
|
// never from these bus capacitors directly. Entity.TrySpendCapacitorCharge/CanSpendCapacitorCharge, Cut 3's
|
|
|
|
|
// named, temporary exception, died with their last caller.
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
public class PowerBus
|
|
|
|
|
{
|
|
|
|
|
private readonly Entity _entity;
|
|
|
|
|
|
|
|
|
|
public PowerBus(Entity entity) => _entity = entity;
|
|
|
|
|
|
|
|
|
|
// Every figure below is this tick's, overwritten wholesale by the next Step -- nothing here is persisted
|
|
|
|
|
// (§0.6); a fresh Entity starts with the defaults below and they read as "fully supplied, nothing drawn"
|
|
|
|
|
// until the first Step runs.
|
|
|
|
|
public float TotalGeneration { get; private set; }
|
|
|
|
|
public float TotalDemand { get; private set; }
|
|
|
|
|
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
// Energy actually delivered this tick, summed across every tier: generation first, then whatever stored
|
|
|
|
|
// capacitor charge covers the rest, capped at demand. Equal to TotalDemand whenever supply covers it; equal
|
|
|
|
|
// to (generation + available charge) otherwise -- never more, which TryConsumeEnergy could never promise (a
|
|
|
|
|
// reactor always paid the remainder as heat, so overdraw was invisible at this level). Which consumers that
|
|
|
|
|
// total actually reaches is what tiering (below) decides; this figure is unaffected by tiers -- the ceiling
|
|
|
|
|
// is a property of supply, not of priority.
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
public float TotalGrant { get; private set; }
|
|
|
|
|
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
// Aggregate fraction of all demand granted this tick (TotalGrant / TotalDemand) -- a fleet-wide summary, not
|
|
|
|
|
// what any one consumer necessarily received. Still written onto every consuming EquippedItem.PowerSupply
|
|
|
|
|
// when every tier in play happens to land on the same ratio (single-tier loadouts, the common case in the
|
|
|
|
|
// Cut 3/4 test fixtures), but a multi-tier shortfall gives different tiers different ratios -- read
|
|
|
|
|
// TierGrantRatio or the item's own PowerSupply for that.
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
public float GrantRatio { get; private set; } = 1f;
|
|
|
|
|
|
F3: an empty tier is not a satisfied tier, and sub-epsilon demand pays its way
AllocateTiers treated any tier whose summed demand was at or below 1e-4 as
"fully met" (ratio 1) without ever subtracting it from `remaining`. Two bugs:
a tier with no consumers equipped in it at all read as 1f, the exact "fully
supplied" signal a UI is meant to trust, even on a fully blacked-out ship
(measured: Critical 0 demand reads "fine" right next to a genuinely starved
High); and a tier with a real but tiny demand (down to 5e-5) was granted out
of nothing and never charged against the ship's actual generation, letting a
lower tier get overfed by the sliver the higher one was never billed for.
AllocateTiers now tracks which tiers actually have a consumer separately from
their summed demand. A tier with no consumer reports the new PowerBus.NoDemand
sentinel (NaN), never 1f. A tier with a consumer whose own request truly is
zero this tick is trivially satisfied (ratio 1, nothing to be short of). Any
other demand, however small, goes through the same grant/remaining arithmetic
as every other tier.
Pinned in tests/Aetheria.Shared.Tests/PowerTierTests.cs:
SubEpsilonDemandGetsNoGrantAtTotalBlackout,
HigherTierSubEpsilonDemandIsStillSubtractedFromRemaining, and
EmptyTierAtBlackoutReportsNoDemandNotFullySupplied -- all fail under the
reverted (<=1e-4 free-pass) mutation. Updated
ATierWithNoDemandPassesItsShareDown's own empty-tier assertion, which had
pinned the bug itself ("empty tiers read as fully met").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:15:30 +02:00
|
|
|
// F3 (docs/stats-and-power-cut.md, Soul pass 2026-09-19): the sentinel AllocateTiers writes for a tier with no
|
|
|
|
|
// consumers equipped in it at all. Distinct from 1f -- an empty tier has not been "fully supplied," there was
|
|
|
|
|
// nothing to supply, and reporting it as 1f is exactly the fully-satisfied signal a UI is meant to read as
|
|
|
|
|
// "this tier is fine," on a ship that may be fully blacked out. A reader must treat NaN as "nothing to report
|
|
|
|
|
// here," not as satisfied or starved.
|
|
|
|
|
public const float NoDemand = float.NaN;
|
|
|
|
|
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
// Cut 5: this tick's within-tier grant ratio, indexed by PowerTiers.Critical..Utility. 1f means that tier's
|
F3: an empty tier is not a satisfied tier, and sub-epsilon demand pays its way
AllocateTiers treated any tier whose summed demand was at or below 1e-4 as
"fully met" (ratio 1) without ever subtracting it from `remaining`. Two bugs:
a tier with no consumers equipped in it at all read as 1f, the exact "fully
supplied" signal a UI is meant to trust, even on a fully blacked-out ship
(measured: Critical 0 demand reads "fine" right next to a genuinely starved
High); and a tier with a real but tiny demand (down to 5e-5) was granted out
of nothing and never charged against the ship's actual generation, letting a
lower tier get overfed by the sliver the higher one was never billed for.
AllocateTiers now tracks which tiers actually have a consumer separately from
their summed demand. A tier with no consumer reports the new PowerBus.NoDemand
sentinel (NaN), never 1f. A tier with a consumer whose own request truly is
zero this tick is trivially satisfied (ratio 1, nothing to be short of). Any
other demand, however small, goes through the same grant/remaining arithmetic
as every other tier.
Pinned in tests/Aetheria.Shared.Tests/PowerTierTests.cs:
SubEpsilonDemandGetsNoGrantAtTotalBlackout,
HigherTierSubEpsilonDemandIsStillSubtractedFromRemaining, and
EmptyTierAtBlackoutReportsNoDemandNotFullySupplied -- all fail under the
reverted (<=1e-4 free-pass) mutation. Updated
ATierWithNoDemandPassesItsShareDown's own empty-tier assertion, which had
pinned the bug itself ("empty tiers read as fully met").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:15:30 +02:00
|
|
|
// demand was fully met; NoDemand (NaN) means the tier has no consumers at all (see NoDemand above); anything
|
|
|
|
|
// else below 1f is exactly the "starved" state a future UI (Cut 8) reads instead of reconstructing it from
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
// per-item PowerSupply. A tier below a starved one is always 0f -- §1.3's "a tier boundary does not leak".
|
|
|
|
|
public float[] TierGrantRatio { get; } = InitFullTiers();
|
|
|
|
|
|
F3: an empty tier is not a satisfied tier, and sub-epsilon demand pays its way
AllocateTiers treated any tier whose summed demand was at or below 1e-4 as
"fully met" (ratio 1) without ever subtracting it from `remaining`. Two bugs:
a tier with no consumers equipped in it at all read as 1f, the exact "fully
supplied" signal a UI is meant to trust, even on a fully blacked-out ship
(measured: Critical 0 demand reads "fine" right next to a genuinely starved
High); and a tier with a real but tiny demand (down to 5e-5) was granted out
of nothing and never charged against the ship's actual generation, letting a
lower tier get overfed by the sliver the higher one was never billed for.
AllocateTiers now tracks which tiers actually have a consumer separately from
their summed demand. A tier with no consumer reports the new PowerBus.NoDemand
sentinel (NaN), never 1f. A tier with a consumer whose own request truly is
zero this tick is trivially satisfied (ratio 1, nothing to be short of). Any
other demand, however small, goes through the same grant/remaining arithmetic
as every other tier.
Pinned in tests/Aetheria.Shared.Tests/PowerTierTests.cs:
SubEpsilonDemandGetsNoGrantAtTotalBlackout,
HigherTierSubEpsilonDemandIsStillSubtractedFromRemaining, and
EmptyTierAtBlackoutReportsNoDemandNotFullySupplied -- all fail under the
reverted (<=1e-4 free-pass) mutation. Updated
ATierWithNoDemandPassesItsShareDown's own empty-tier assertion, which had
pinned the bug itself ("empty tiers read as fully met").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:15:30 +02:00
|
|
|
// Before the bus has ever run a Step, there is no grant information at all yet -- not "fully supplied,"
|
|
|
|
|
// "nothing to report" (NoDemand), same as an empty tier reads once Step does run.
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
private static float[] InitFullTiers()
|
|
|
|
|
{
|
|
|
|
|
var ratios = new float[PowerTiers.Count];
|
F3: an empty tier is not a satisfied tier, and sub-epsilon demand pays its way
AllocateTiers treated any tier whose summed demand was at or below 1e-4 as
"fully met" (ratio 1) without ever subtracting it from `remaining`. Two bugs:
a tier with no consumers equipped in it at all read as 1f, the exact "fully
supplied" signal a UI is meant to trust, even on a fully blacked-out ship
(measured: Critical 0 demand reads "fine" right next to a genuinely starved
High); and a tier with a real but tiny demand (down to 5e-5) was granted out
of nothing and never charged against the ship's actual generation, letting a
lower tier get overfed by the sliver the higher one was never billed for.
AllocateTiers now tracks which tiers actually have a consumer separately from
their summed demand. A tier with no consumer reports the new PowerBus.NoDemand
sentinel (NaN), never 1f. A tier with a consumer whose own request truly is
zero this tick is trivially satisfied (ratio 1, nothing to be short of). Any
other demand, however small, goes through the same grant/remaining arithmetic
as every other tier.
Pinned in tests/Aetheria.Shared.Tests/PowerTierTests.cs:
SubEpsilonDemandGetsNoGrantAtTotalBlackout,
HigherTierSubEpsilonDemandIsStillSubtractedFromRemaining, and
EmptyTierAtBlackoutReportsNoDemandNotFullySupplied -- all fail under the
reverted (<=1e-4 free-pass) mutation. Updated
ATierWithNoDemandPassesItsShareDown's own empty-tier assertion, which had
pinned the bug itself ("empty tiers read as fully met").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:15:30 +02:00
|
|
|
for (var i = 0; i < ratios.Length; i++) ratios[i] = NoDemand;
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
return ratios;
|
|
|
|
|
}
|
|
|
|
|
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
// What Reactor.Execute reads to run its own heat/throttle arithmetic (Cut 3: "the arithmetic survives, moved
|
|
|
|
|
// under the bus's numbers"). Positive: demand left unmet after generation AND stored charge, split evenly
|
|
|
|
|
// across online reactors for overload heat. Negative: generation left over after demand and after topping up
|
|
|
|
|
// every capacitor, split evenly for the throttling arithmetic. Reactor.Draw no longer feeds this -- it is now
|
|
|
|
|
// a reported total, not a sink (§1.2).
|
|
|
|
|
public float NetDraw { get; private set; }
|
|
|
|
|
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
// One equipped item's power request for this tick, captured once so PowerRequest -- which some behaviours
|
|
|
|
|
// (Shield, InstantWeapon, Sensor) implement by refreshing their own cached stats -- is never called twice in
|
|
|
|
|
// one Step.
|
|
|
|
|
private readonly struct Draw
|
|
|
|
|
{
|
|
|
|
|
public readonly EquippedItem Item;
|
|
|
|
|
public readonly int Tier;
|
|
|
|
|
public readonly float Request;
|
|
|
|
|
public Draw(EquippedItem item, int tier, float request) { Item = item; Tier = tier; Request = request; }
|
|
|
|
|
}
|
|
|
|
|
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
public void Step(float dt)
|
|
|
|
|
{
|
|
|
|
|
var reactors = new List<Reactor>();
|
|
|
|
|
var capacitors = new List<Capacitor>();
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
var draws = new List<Draw>();
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
foreach (var item in _entity.Equipment)
|
|
|
|
|
foreach (var behavior in item.Behaviors)
|
|
|
|
|
{
|
|
|
|
|
switch (behavior)
|
|
|
|
|
{
|
2026-09-19 05:06:31 +02:00
|
|
|
// F2 (docs/stats-and-power-cut.md, Soul pass 2026-09-19): this must agree with the one predicate
|
|
|
|
|
// Entity.Update actually gates Execute on (Active.Value, Entity.cs -- Enabled && Online), in both
|
|
|
|
|
// directions. Gating on Online alone let a reactor the player switched off (Enabled = false, so
|
|
|
|
|
// Active is false but Online stays true) keep generating here even though Reactor.Execute -- the
|
|
|
|
|
// behaviour that produces the heat receipt -- never runs for it: free, heat-free power. Capacitor
|
|
|
|
|
// has no IPowerConsumer/generation role and is unconditionally a store regardless of its own
|
|
|
|
|
// item's state (mirrors pre-existing behaviour; nothing in the ruling asks for that to change).
|
|
|
|
|
case Reactor reactor when item.Active.Value:
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
reactors.Add(reactor);
|
|
|
|
|
break;
|
|
|
|
|
case Capacitor capacitor:
|
|
|
|
|
capacitors.Add(capacitor);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-09-19 05:06:31 +02:00
|
|
|
// The other direction of the same rule: a consumer switched off or shut down (Active.Value false)
|
|
|
|
|
// never runs its Execute, so it must never be billed either -- otherwise a disabled item's demand
|
|
|
|
|
// starves every live consumer and drains the capacitors for energy nobody spends.
|
|
|
|
|
if (behavior is IPowerConsumer consumer && item.Active.Value)
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
{
|
|
|
|
|
// Cut 5 (docs/stats-and-power-cut.md §1.3): the item's own stored choice wins once it has one;
|
|
|
|
|
// PowerTiers.Unassigned only survives past EquippedItem's constructor for a consumer added to
|
|
|
|
|
// the game after this unit was minted, which the constructor never saw -- fall back to the
|
|
|
|
|
// behaviour's own default rather than stranding it at an invalid tier.
|
|
|
|
|
var tier = item.EquippableItem.PowerTier;
|
|
|
|
|
if (tier == PowerTiers.Unassigned) tier = consumer.DefaultPowerTier;
|
|
|
|
|
tier = clamp(tier, 0, PowerTiers.Count - 1);
|
|
|
|
|
draws.Add(new Draw(item, tier, max(0f, consumer.PowerRequest(dt))));
|
|
|
|
|
}
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TotalGeneration = reactors.Sum(r => r.Generation(dt));
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
TotalDemand = draws.Sum(d => d.Request);
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
|
|
|
|
|
var availableCharge = capacitors.Sum(c => c.Charge);
|
|
|
|
|
var preCapacitorNet = TotalDemand - TotalGeneration;
|
|
|
|
|
|
|
|
|
|
float overload = 0f, unabsorbedSurplus = 0f;
|
|
|
|
|
if (preCapacitorNet > 0f)
|
|
|
|
|
{
|
|
|
|
|
var chargeDrawn = min(preCapacitorNet, availableCharge);
|
|
|
|
|
if (chargeDrawn > 0f) DrainEvenly(capacitors, chargeDrawn);
|
|
|
|
|
overload = preCapacitorNet - chargeDrawn;
|
|
|
|
|
}
|
|
|
|
|
else if (preCapacitorNet < 0f)
|
|
|
|
|
{
|
|
|
|
|
unabsorbedSurplus = FillEvenly(capacitors, -preCapacitorNet);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NetDraw = overload - unabsorbedSurplus; // exactly one of the two is ever nonzero
|
|
|
|
|
TotalGrant = TotalDemand - overload;
|
|
|
|
|
GrantRatio = TotalDemand <= 1e-4f ? 1f : saturate(TotalGrant / TotalDemand);
|
|
|
|
|
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
AllocateTiers(draws);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cut 5 (docs/stats-and-power-cut.md §1.3): walks tiers lowest-number-first, feeding each in full before the
|
|
|
|
|
// next sees anything. remaining only ever shrinks, and once it hits 0 every later tier's ratio is 0 by the
|
|
|
|
|
// same division -- a lower tier can never take from a higher one (§1.3 "a tier boundary does not leak"),
|
|
|
|
|
// because a higher tier's share is committed (remaining -= grant) before a lower tier is even considered.
|
|
|
|
|
// Within a tier every consumer shares that tier's one ratio against its own request, so two unequal requests
|
|
|
|
|
// still divide proportionally -- the same property Cut 3 proved for the whole ship, now proved per tier.
|
|
|
|
|
private void AllocateTiers(List<Draw> draws)
|
|
|
|
|
{
|
|
|
|
|
var tierDemand = new float[PowerTiers.Count];
|
F3: an empty tier is not a satisfied tier, and sub-epsilon demand pays its way
AllocateTiers treated any tier whose summed demand was at or below 1e-4 as
"fully met" (ratio 1) without ever subtracting it from `remaining`. Two bugs:
a tier with no consumers equipped in it at all read as 1f, the exact "fully
supplied" signal a UI is meant to trust, even on a fully blacked-out ship
(measured: Critical 0 demand reads "fine" right next to a genuinely starved
High); and a tier with a real but tiny demand (down to 5e-5) was granted out
of nothing and never charged against the ship's actual generation, letting a
lower tier get overfed by the sliver the higher one was never billed for.
AllocateTiers now tracks which tiers actually have a consumer separately from
their summed demand. A tier with no consumer reports the new PowerBus.NoDemand
sentinel (NaN), never 1f. A tier with a consumer whose own request truly is
zero this tick is trivially satisfied (ratio 1, nothing to be short of). Any
other demand, however small, goes through the same grant/remaining arithmetic
as every other tier.
Pinned in tests/Aetheria.Shared.Tests/PowerTierTests.cs:
SubEpsilonDemandGetsNoGrantAtTotalBlackout,
HigherTierSubEpsilonDemandIsStillSubtractedFromRemaining, and
EmptyTierAtBlackoutReportsNoDemandNotFullySupplied -- all fail under the
reverted (<=1e-4 free-pass) mutation. Updated
ATierWithNoDemandPassesItsShareDown's own empty-tier assertion, which had
pinned the bug itself ("empty tiers read as fully met").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:15:30 +02:00
|
|
|
// F3 (docs/stats-and-power-cut.md, Soul pass 2026-09-19): demand alone cannot tell "no consumer is
|
|
|
|
|
// equipped in this tier" from "a real consumer is equipped here and its own request happens to be 0 (or
|
|
|
|
|
// effectively 0) this tick" -- both sum to the same tierDemand. Only the first is actually empty; the
|
|
|
|
|
// second still has an item reading this tier's ratio back as its own PowerSupply, and that item is owed
|
|
|
|
|
// a real answer (trivially 1: it asked for nothing, so nothing it could be short of), not NoDemand.
|
|
|
|
|
var tierHasConsumer = new bool[PowerTiers.Count];
|
|
|
|
|
foreach (var draw in draws) { tierDemand[draw.Tier] += draw.Request; tierHasConsumer[draw.Tier] = true; }
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
|
|
|
|
|
var remaining = TotalGrant;
|
|
|
|
|
var ratios = TierGrantRatio;
|
|
|
|
|
for (var tier = 0; tier < PowerTiers.Count; tier++)
|
|
|
|
|
{
|
F3: an empty tier is not a satisfied tier, and sub-epsilon demand pays its way
AllocateTiers treated any tier whose summed demand was at or below 1e-4 as
"fully met" (ratio 1) without ever subtracting it from `remaining`. Two bugs:
a tier with no consumers equipped in it at all read as 1f, the exact "fully
supplied" signal a UI is meant to trust, even on a fully blacked-out ship
(measured: Critical 0 demand reads "fine" right next to a genuinely starved
High); and a tier with a real but tiny demand (down to 5e-5) was granted out
of nothing and never charged against the ship's actual generation, letting a
lower tier get overfed by the sliver the higher one was never billed for.
AllocateTiers now tracks which tiers actually have a consumer separately from
their summed demand. A tier with no consumer reports the new PowerBus.NoDemand
sentinel (NaN), never 1f. A tier with a consumer whose own request truly is
zero this tick is trivially satisfied (ratio 1, nothing to be short of). Any
other demand, however small, goes through the same grant/remaining arithmetic
as every other tier.
Pinned in tests/Aetheria.Shared.Tests/PowerTierTests.cs:
SubEpsilonDemandGetsNoGrantAtTotalBlackout,
HigherTierSubEpsilonDemandIsStillSubtractedFromRemaining, and
EmptyTierAtBlackoutReportsNoDemandNotFullySupplied -- all fail under the
reverted (<=1e-4 free-pass) mutation. Updated
ATierWithNoDemandPassesItsShareDown's own empty-tier assertion, which had
pinned the bug itself ("empty tiers read as fully met").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:15:30 +02:00
|
|
|
// A tier with genuinely no consumers is not satisfied, it is empty: report NoDemand, not the "fully
|
|
|
|
|
// supplied" signal 1f. Nothing to subtract from `remaining` either way.
|
|
|
|
|
if (!tierHasConsumer[tier]) { ratios[tier] = NoDemand; continue; }
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
var demand = tierDemand[tier];
|
F3: an empty tier is not a satisfied tier, and sub-epsilon demand pays its way
AllocateTiers treated any tier whose summed demand was at or below 1e-4 as
"fully met" (ratio 1) without ever subtracting it from `remaining`. Two bugs:
a tier with no consumers equipped in it at all read as 1f, the exact "fully
supplied" signal a UI is meant to trust, even on a fully blacked-out ship
(measured: Critical 0 demand reads "fine" right next to a genuinely starved
High); and a tier with a real but tiny demand (down to 5e-5) was granted out
of nothing and never charged against the ship's actual generation, letting a
lower tier get overfed by the sliver the higher one was never billed for.
AllocateTiers now tracks which tiers actually have a consumer separately from
their summed demand. A tier with no consumer reports the new PowerBus.NoDemand
sentinel (NaN), never 1f. A tier with a consumer whose own request truly is
zero this tick is trivially satisfied (ratio 1, nothing to be short of). Any
other demand, however small, goes through the same grant/remaining arithmetic
as every other tier.
Pinned in tests/Aetheria.Shared.Tests/PowerTierTests.cs:
SubEpsilonDemandGetsNoGrantAtTotalBlackout,
HigherTierSubEpsilonDemandIsStillSubtractedFromRemaining, and
EmptyTierAtBlackoutReportsNoDemandNotFullySupplied -- all fail under the
reverted (<=1e-4 free-pass) mutation. Updated
ATierWithNoDemandPassesItsShareDown's own empty-tier assertion, which had
pinned the bug itself ("empty tiers read as fully met").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:15:30 +02:00
|
|
|
// This used to short-circuit ANY demand at or below 1e-4 the same way as a genuinely empty tier,
|
|
|
|
|
// which both reported a grant nobody made (an item reading ratio 1 while the ship generates nothing,
|
|
|
|
|
// down to Soul's own probe at 5e-5) and leaked that tier's unsubtracted share to every lower one. A
|
|
|
|
|
// consumer requesting truly nothing (demand <= 0f exactly) is trivially fully met; anything above
|
|
|
|
|
// that, however tiny, is rationed like any other tier.
|
|
|
|
|
if (demand <= 0f) { ratios[tier] = 1f; continue; }
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
var grant = min(demand, remaining);
|
|
|
|
|
ratios[tier] = saturate(grant / demand);
|
|
|
|
|
remaining -= grant;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var draw in draws)
|
Cut 6: wire StatSource.PowerSupply and refuse a power request that reads it
docs/stats-and-power-cut.md Cut 6, purely additive per its own map: the enum member, the
resolver's generation-cache and the bus's PowerSupply write already existed (Cuts 2/3/5)
as an identity everywhere. This lands the two things Cut 6 actually adds.
The curve: EquippedItem.PowerSupplyFactor now returns pow(PowerSupply, exponent) instead
of the Cut-3 identity stub, so a stat that declares a PowerSupply term degrades as this
tick's grant falls and reads unchanged at full supply (pow(1, n) == 1 regardless of n).
PowerBus.AllocateTiers now invalidates StatSource.PowerSupply once per draw per tick -- the
missing half of the wiring named in the cut map -- so the resolver actually recomputes when
the grant moves, using the same per-source generation scheme Heat/Durability already use
(unconditional, at most once per tick, never touching a stat that did not declare the
source).
The request-independence rule: a stat that decides a power request may not depend on power
supply, directly or through a modifier chain. Direct dependency is checked statically
(StatValidation.ValidateNoPowerSupplyOnRequest, ItemData.cs) against a fixed registry of
(BehaviorData type, field) pairs -- the one PerformanceStat each IPowerConsumer's own
PowerRequest evaluates as its request, a compile-time fact rather than something authored
per instance -- and runs at catalog load (AetheriaStores.Open), at Upsert, and again at
equip (EquippedItem's constructor) as defense against the two named bypasses that write a
catalog document without going through Upsert. The modifier-chain half is dynamic
(StatModifier.ValidateNoPowerSupplyChain): a modifier's own magnitude stat is walked
through the same magnitude->target edges ValidateNoCycle already builds for the self-cycle
check, and refused at Initialize (equip time) if it is power-tainted and targets a known
request stat, naming both items.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 03:47:07 +02:00
|
|
|
{
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
draw.Item.PowerSupply = ratios[draw.Tier];
|
Cut 6: wire StatSource.PowerSupply and refuse a power request that reads it
docs/stats-and-power-cut.md Cut 6, purely additive per its own map: the enum member, the
resolver's generation-cache and the bus's PowerSupply write already existed (Cuts 2/3/5)
as an identity everywhere. This lands the two things Cut 6 actually adds.
The curve: EquippedItem.PowerSupplyFactor now returns pow(PowerSupply, exponent) instead
of the Cut-3 identity stub, so a stat that declares a PowerSupply term degrades as this
tick's grant falls and reads unchanged at full supply (pow(1, n) == 1 regardless of n).
PowerBus.AllocateTiers now invalidates StatSource.PowerSupply once per draw per tick -- the
missing half of the wiring named in the cut map -- so the resolver actually recomputes when
the grant moves, using the same per-source generation scheme Heat/Durability already use
(unconditional, at most once per tick, never touching a stat that did not declare the
source).
The request-independence rule: a stat that decides a power request may not depend on power
supply, directly or through a modifier chain. Direct dependency is checked statically
(StatValidation.ValidateNoPowerSupplyOnRequest, ItemData.cs) against a fixed registry of
(BehaviorData type, field) pairs -- the one PerformanceStat each IPowerConsumer's own
PowerRequest evaluates as its request, a compile-time fact rather than something authored
per instance -- and runs at catalog load (AetheriaStores.Open), at Upsert, and again at
equip (EquippedItem's constructor) as defense against the two named bypasses that write a
catalog document without going through Upsert. The modifier-chain half is dynamic
(StatModifier.ValidateNoPowerSupplyChain): a modifier's own magnitude stat is walked
through the same magnitude->target edges ValidateNoCycle already builds for the self-cycle
check, and refused at Initialize (equip time) if it is power-tainted and targets a known
request stat, naming both items.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 03:47:07 +02:00
|
|
|
// Cut 6 (docs/stats-and-power-cut.md): the missing half of the wiring -- a stat with a PowerSupply
|
|
|
|
|
// term must recompute when the grant actually moves. Called unconditionally, once per draw per tick,
|
|
|
|
|
// the same shape as EquippedItem.UpdatePerformance's own Heat/Durability invalidation: "recomputes at
|
|
|
|
|
// most once per tick per (item, stat)," not "only when the value moved." An item with no PowerSupply
|
|
|
|
|
// term pays nothing extra -- the resolver's per-source generation bookkeeping (StatResolver.Resolve)
|
|
|
|
|
// only ever looks at sources a stat's own Terms declared.
|
|
|
|
|
_entity.Resolver.InvalidateSource(draw.Item, StatSource.PowerSupply);
|
|
|
|
|
}
|
Cut 3: PowerBus replaces TryConsumeEnergy/CanConsumeEnergy
Deletes Entity.CanConsumeEnergy/TryConsumeEnergy and Reactor's own capacitor
draining/filling (the demand-mechanism halves of Reactor.cs:90-141); adds
PowerBus (one per Entity, stepped once per tick before equipment executes),
IPowerConsumer on the five continuous draw sites (EnergyDraw, Thruster,
AetherDrive, Radiator, ConstantWeapon), and EquippedItem.PowerSupply as the
bus's grant ratio. Reactor keeps only its heat/throttle arithmetic, now driven
by PowerBus.NetDraw instead of an accumulator any behaviour could add into.
The four instant draws (InstantWeapon's burst/shot, Sensor's ping, Shield's hit
taken) are out of scope for the bus this cut per the map; they spend capacitor
charge directly and atomically via the new Entity.TrySpendCapacitorCharge/
CanSpendCapacitorCharge, replacing TryConsumeEnergy's old partial-drain-then-
refuse bug for that path too. Named, temporary exception closed by Cut 4.
docs/stats-and-power-cut.md Cut 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:39:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mirrors Entity.TryConsumeEnergy's old do/while exactly (draw evenly across every capacitor that still has
|
|
|
|
|
// charge, repeating as capacitors empty out), just relocated: bus capacitors are drained only by the bus now.
|
|
|
|
|
private static void DrainEvenly(List<Capacitor> capacitors, float amount)
|
|
|
|
|
{
|
|
|
|
|
int chargedCount;
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
chargedCount = capacitors.Count(c => c.Charge > .01f);
|
|
|
|
|
if (chargedCount == 0) break;
|
|
|
|
|
var share = amount / chargedCount;
|
|
|
|
|
foreach (var cap in capacitors)
|
|
|
|
|
{
|
|
|
|
|
if (cap.Charge <= .01f) continue;
|
|
|
|
|
var drawn = min(share, cap.Charge);
|
|
|
|
|
cap.AddCharge(-drawn);
|
|
|
|
|
amount -= drawn;
|
|
|
|
|
}
|
|
|
|
|
} while (chargedCount > 0 && amount > .01f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fills every capacitor evenly up to its own capacity and returns whatever would not fit, for Reactor to
|
|
|
|
|
// throttle away -- the same job Reactor.cs used to do to itself (surplus branch), moved here because bus
|
|
|
|
|
// capacitors are charged only by the bus (§0b). Resolves Capacity itself rather than reading each
|
|
|
|
|
// Capacitor's cached property: the bus steps before any behaviour's Execute runs this tick, so that property
|
|
|
|
|
// still holds last tick's value.
|
|
|
|
|
private static float FillEvenly(List<Capacitor> capacitors, float amount)
|
|
|
|
|
{
|
|
|
|
|
int nonFullCount;
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
nonFullCount = capacitors.Count(c => c.Charge < c.ResolveCapacity() - .01f);
|
|
|
|
|
if (nonFullCount == 0) break;
|
|
|
|
|
var share = amount / nonFullCount;
|
|
|
|
|
foreach (var cap in capacitors)
|
|
|
|
|
{
|
|
|
|
|
var capacity = cap.ResolveCapacity();
|
|
|
|
|
if (cap.Charge >= capacity - .01f) continue;
|
|
|
|
|
var added = min(share, capacity - cap.Charge);
|
|
|
|
|
cap.AddCharge(added);
|
|
|
|
|
amount -= added;
|
|
|
|
|
}
|
|
|
|
|
} while (nonFullCount > 0 && amount > .01f);
|
|
|
|
|
return amount;
|
|
|
|
|
}
|
|
|
|
|
}
|