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>
143 lines
6.9 KiB
C#
143 lines
6.9 KiB
C#
/* 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
|
|
// IPowerConsumer. The four instant draws the cut map names (a burst, a shot, a ping, a hit taken) are out of
|
|
// scope for this cut and spend capacitor charge directly instead, through Entity.TrySpendCapacitorCharge -- a
|
|
// named, temporary exception this cut accepts, closed by Cut 4.
|
|
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;
|
|
}
|
|
}
|