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
|
|
|
/* 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);
|
|
|
|
|
|
2026-10-23 00:09:21 +02:00
|
|
|
// A weapon held through a step may fire several rounds in it (ruling rounds-per-step): the charge it holds and what flows in
|
|
|
|
|
// during the step pay for them, so for that step its buffer is a full activation plus the step's inflow; Settle trims what
|
|
|
|
|
// the step did not spend back to Capacity. The fill rate is unchanged, so the energy a gun draws is still exactly its
|
|
|
|
|
// rounds' cost. A sensor or a shield, and a weapon not held, keep the one-activation buffer above.
|
|
|
|
|
private float StepReach(float dt) => Capacity + Rate * dt;
|
2026-10-23 00:02:49 +02:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
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
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|