Aetheria/Assets/Scripts/ServerShared/Behaviors/InputCapacitor.cs

71 lines
4.4 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 static CultMath.math;
// Cut 4 (docs/stats-and-power-cut.md §1.2, §0b "An input capacitor"): the input-only buffer that lets an
// instant activation (a shot, a ping, a shield's damage reserve) draw from the PowerBus continuously instead
// of spending stored charge directly -- closing Cut 3's named, temporary exception
// (Entity.TrySpendCapacitorCharge/CanSpendCapacitorCharge, both deleted with their last caller this cut).
//
// Not a Behavior of its own: per §0b, an input capacitor is identified by the Behavior instance that owns it
// (InstantWeapon, Sensor, Shield), which holds one of these as a plain field. Two writers, disjoint signs,
// named: the bus fills it, through AddCharge, called only from the owning behaviour's own Execute after it
// reads back its granted fraction from Item.PowerSupply; the owning behaviour drains it, through TrySpend,
// called only when it actually activates. Nothing else may touch Charge.
public class InputCapacitor
{
public float Capacity { get; private set; }
public float Rate { get; private set; }
public float Charge { get; private set; }
// Cut 4 (§7 Q4, operator ruling): "derived from the item's Energy and Cooldown ... with an authored
// override" so burst capacity and sustained rate stay separate levers. capacityOverride/rateOverride of 0
// mean "not authored, use the derived default" -- the same identity-when-absent convention PerformanceStat
// itself already uses (a term-less stat resolves to its identity factor). Cutting Charge down here (rather
// than only clamping in AddCharge) is what lets a Capacity that shrinks under a stat change immediately
// stop promising a shot it can no longer pay for.
public void UpdateStats(float energy, float cooldown, float capacityOverride = 0f, float rateOverride = 0f)
{
Capacity = capacityOverride > 0f ? capacityOverride : max(0f, energy);
Rate = rateOverride > 0f ? rateOverride : (cooldown > 0f ? Capacity / cooldown : Capacity);
Charge = clamp(Charge, 0f, Capacity);
}
// What PowerBus should count as this consumer's demand this tick (via the owning behaviour's
// IPowerConsumer.PowerRequest), and what that behaviour should scale by its own Item.PowerSupply and pass
// to AddCharge once its own Execute runs. The two calls must agree on dt and must not have let Charge move
// in between, because PowerBus.Step calls this before any behaviour's Execute runs this tick (§1.2:
// "computable from state that is already current at that point").
public float RequestedFill(float dt) => min(Rate * dt, max(0f, Capacity - Charge));
public void AddCharge(float amount) => Charge = clamp(Charge + amount, 0f, Capacity);
// A weapon may fire several rounds in one step (ruling rounds-per-step), and what flows in during that step is what pays
// for them, so its buffer is Capacity or the step's inflow, whichever is more; Settle trims the surplus a step did not spend.
// The fill rate is unchanged, so the energy a gun draws is still exactly its rounds' cost. A sensor or a shield keeps the
// one-activation buffer above.
private float StepReach(float dt) => max(Capacity, Rate * dt);
public float RequestedStepFill(float dt) => min(Rate * dt, max(0f, StepReach(dt) - Charge));
public void AddStepCharge(float amount, float dt) => Charge = clamp(Charge + amount, 0f, StepReach(dt));
public void Settle() => Charge = min(Charge, Capacity);
public bool CanSpend(float cost) => Charge >= cost;
// Atomic and whole-or-nothing, mirroring Entity.TrySpendCapacitorCharge's old contract exactly (down to the
// near-zero shortcut) but scoped to this one behaviour's own buffer instead of the entity's shared bus
// capacitors: either the cost is covered in full, or nothing moves. This is the mechanism behind the
// operator's ruling that no item ever receives a fraction of a shot -- callers that want that guarantee
// spend exactly Capacity, which only ever succeeds at full charge.
public bool TrySpend(float cost)
{
if (cost < .01f) return true;
if (!CanSpend(cost)) return false;
Charge -= cost;
return true;
}
}