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.
|
|
|
|
|
// There are no tiers yet (Cut 5 adds them), so a shortfall rations the whole pool by the same fraction for every
|
|
|
|
|
// consumer alike -- the death of the old "whoever equipped first drains the capacitors first" hidden priority
|
|
|
|
|
// (§0.5), not a replacement for it.
|
|
|
|
|
//
|
|
|
|
|
// 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; }
|
|
|
|
|
|
|
|
|
|
// Energy actually delivered this tick: 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).
|
|
|
|
|
public float TotalGrant { get; private set; }
|
|
|
|
|
|
|
|
|
|
// Fraction of every consumer's own request actually granted this tick, shared by every consumer alike (no
|
|
|
|
|
// tiers yet). Also written onto every consuming EquippedItem.PowerSupply.
|
|
|
|
|
public float GrantRatio { get; private set; } = 1f;
|
|
|
|
|
|
|
|
|
|
// 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; }
|
|
|
|
|
|
|
|
|
|
public void Step(float dt)
|
|
|
|
|
{
|
|
|
|
|
var reactors = new List<Reactor>();
|
|
|
|
|
var capacitors = new List<Capacitor>();
|
|
|
|
|
var consumers = new List<IPowerConsumer>();
|
|
|
|
|
foreach (var item in _entity.Equipment)
|
|
|
|
|
foreach (var behavior in item.Behaviors)
|
|
|
|
|
{
|
|
|
|
|
switch (behavior)
|
|
|
|
|
{
|
|
|
|
|
case Reactor reactor when reactor.Item.Online.Value:
|
|
|
|
|
reactors.Add(reactor);
|
|
|
|
|
break;
|
|
|
|
|
case Capacitor capacitor:
|
|
|
|
|
capacitors.Add(capacitor);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (behavior is IPowerConsumer consumer)
|
|
|
|
|
consumers.Add(consumer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TotalGeneration = reactors.Sum(r => r.Generation(dt));
|
|
|
|
|
TotalDemand = consumers.Sum(c => max(0f, c.PowerRequest(dt)));
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
foreach (var item in _entity.Equipment)
|
|
|
|
|
if (item.Behaviors.Any(b => b is IPowerConsumer))
|
|
|
|
|
item.PowerSupply = GrantRatio;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|