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>
This commit is contained in:
parent
bfdde9c33d
commit
f1fd64ac60
15 changed files with 707 additions and 135 deletions
|
|
@ -61,7 +61,7 @@ public class AetherDriveData : BehaviorData
|
|||
}
|
||||
}
|
||||
|
||||
public class AetherDrive : Behavior
|
||||
public class AetherDrive : Behavior, IPowerConsumer
|
||||
{
|
||||
private AetherDriveData _data;
|
||||
private float3 _axis;
|
||||
|
|
@ -89,6 +89,28 @@ public class AetherDrive : Behavior
|
|||
_data = data;
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md): the request PowerBus needs before Execute runs. Only the rotor
|
||||
// spin-up (torque accelerating Rpm toward MaximumRpm) costs power -- spending existing spin into thrust
|
||||
// (Execute's decay-to-thrust arithmetic below) is free, same as before. Pure and side-effect-free: it reads
|
||||
// Rpm but does not write it, so Execute's own identical arithmetic a few lines later -- which does perform
|
||||
// the real decay -- produces the same numbers Rpm actually moves by. Accepts the recompute (Q3's ruling).
|
||||
public float PowerRequest(float dt)
|
||||
{
|
||||
var couplingLambda = _data.CouplingLambda * Item.Evaluate(_data.LambdaMultiplier) * max(abs(_axis), Evaluate(_data.PassiveCoupling));
|
||||
var rpmAfterDecay = decay(Rpm, couplingLambda, dt);
|
||||
var maximumRpm = Evaluate(_data.MaximumRpm);
|
||||
var torqueProfile = float3(
|
||||
_data.TorqueProfile.Evaluate(rpmAfterDecay.x / maximumRpm),
|
||||
_data.TorqueProfile.Evaluate(rpmAfterDecay.y / maximumRpm),
|
||||
_data.TorqueProfile.Evaluate(rpmAfterDecay.z / maximumRpm));
|
||||
var potentialTorque = Evaluate(_data.Torque) * torqueProfile;
|
||||
var potentialRpmDelta = potentialTorque / length(_data.RotorMass) * dt;
|
||||
var actualRpmDelta = min(maximumRpm - rpmAfterDecay, potentialRpmDelta);
|
||||
var torqueRatio = actualRpmDelta / potentialRpmDelta;
|
||||
var draw = torqueRatio * Evaluate(_data.EnergyDraw) / 3;
|
||||
return (draw.x + draw.y + draw.z) * dt;
|
||||
}
|
||||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
var rotorSpeed = Rpm * _data.RotorDiameter / 100;
|
||||
|
|
@ -129,19 +151,18 @@ public class AetherDrive : Behavior
|
|||
var potentialRpmDelta = potentialTorque / length(_data.RotorMass) * dt;
|
||||
var actualRpmDelta = min(MaximumRpm - Rpm, potentialRpmDelta);
|
||||
var torqueRatio = actualRpmDelta / potentialRpmDelta;
|
||||
var draw = torqueRatio * Evaluate(_data.EnergyDraw) / 3;
|
||||
|
||||
|
||||
Item.SetAudioParameter(SpecialAudioParameter.Intensity, max(max(abs(_axis.x), abs(_axis.y)), abs(_axis.z)));
|
||||
Item.SetAudioParameter(_data.RpmAudioParameter, (Rpm.x + Rpm.y + Rpm.z) / 3 / MaximumRpm);
|
||||
Item.SetAudioParameter(_data.TorqueRatioAudioParameter, max(max(torqueRatio.x, torqueRatio.y), torqueRatio.z));
|
||||
|
||||
if (Entity.TryConsumeEnergy((draw.x + draw.y + draw.z)*dt))
|
||||
if (Item.PowerSupply >= 1f)
|
||||
{
|
||||
Rpm += actualRpmDelta;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -134,6 +134,17 @@ public interface IPopulationAssignment
|
|||
int AssignedPopulation { get; set; }
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md §1.2): a behaviour that wants to draw from its entity's PowerBus declares a
|
||||
// request instead of spending energy itself. PowerBus.Step calls this on every consumer once per tick, before
|
||||
// any behaviour's Execute runs, so PowerRequest must be computable from state that is already current at that
|
||||
// point -- a resolved stat and, where the map calls for one, a behaviour-supplied scalar such as Thruster's
|
||||
// throttle axis (set externally before the tick) -- never from work Execute itself would otherwise do. The
|
||||
// behaviour reads back how much of its own request was granted from Item.PowerSupply during its own Execute.
|
||||
public interface IPowerConsumer
|
||||
{
|
||||
float PowerRequest(float dt);
|
||||
}
|
||||
|
||||
[Inspectable,
|
||||
Union(0, typeof(GuidedWeaponData)),
|
||||
Union(1, typeof(LauncherData)),
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ public class Capacitor : Behavior
|
|||
AddHeat(abs(charge) * (1-Efficiency));
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md): PowerBus needs Capacity before this behaviour's own Execute runs this
|
||||
// tick (the bus steps first, §1.2), so it resolves the stat itself rather than reading the cached property,
|
||||
// which still holds last tick's value at that point.
|
||||
public float ResolveCapacity() => Evaluate(_data.Capacity);
|
||||
|
||||
public Capacitor(CapacitorData data, EquippedItem item) : base(data, item)
|
||||
{
|
||||
_data = data;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public class ConstantWeaponData : WeaponData
|
|||
}
|
||||
}
|
||||
|
||||
public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior
|
||||
public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
|
||||
{
|
||||
private ConstantWeaponData _data;
|
||||
private int _ammo = 1;
|
||||
|
|
@ -72,6 +72,11 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior
|
|||
_data = data;
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md): the request the bus needs before Execute runs -- what continuing to
|
||||
// fire this tick would cost, or nothing when not firing or safed. _firing is set externally (Activate/
|
||||
// Deactivate) before Entity.Update calls PowerBus.Step, so it is already current when this runs.
|
||||
public float PowerRequest(float dt) => _firing && StanceAllowsFire ? Evaluate(_data.Energy) * dt : 0f;
|
||||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
base.Execute(dt);
|
||||
|
|
@ -84,7 +89,7 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior
|
|||
}
|
||||
if (_firing)
|
||||
{
|
||||
if (!Entity.TryConsumeEnergy(Evaluate(_data.Energy) * dt))
|
||||
if (Item != null && Item.PowerSupply < 1f)
|
||||
{
|
||||
_firing = false;
|
||||
OnStopFiring?.Invoke();
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public class EnergyDrawData : BehaviorData
|
|||
}
|
||||
}
|
||||
|
||||
public class EnergyDraw : Behavior
|
||||
public class EnergyDraw : Behavior, IPowerConsumer
|
||||
{
|
||||
private EnergyDrawData _data;
|
||||
|
||||
|
|
@ -41,8 +41,15 @@ public class EnergyDraw : Behavior
|
|||
_data = data;
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md): the request PowerBus needs before Execute runs, in place of the
|
||||
// direct Entity.TryConsumeEnergy spend that used to happen inside Execute.
|
||||
public float PowerRequest(float dt) => Evaluate(_data.EnergyDraw) * (_data.PerSecond ? dt : 1);
|
||||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
return Entity.TryConsumeEnergy(Evaluate(_data.EnergyDraw) * (_data.PerSecond ? dt : 1));
|
||||
// A consumable-hosted instance (Item null) has no PowerBus entry (§1.2's grants are keyed by
|
||||
// EquippedItem); named rather than silently assumed away, it always succeeds here, the same way every
|
||||
// other context-dependent factor a ConsumableItemEffect answers with the identity elsewhere in this cut.
|
||||
return Item == null || Item.PowerSupply >= 1f;
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +95,10 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior
|
|||
|
||||
// If 1 ammo is consumed per burst, perform ammo and energy consumption here
|
||||
// UseAmmo returns false when triggering reload; cancel firing if that is the case
|
||||
if(_data.SingleAmmoBurst && (!Entity.TryConsumeEnergy(Energy) || !UseAmmo())) return;
|
||||
// Cut 3 (docs/stats-and-power-cut.md): one of the four instant draws out of scope for the bus this cut
|
||||
// (a burst) -- named, temporary exception, spending capacitor charge directly. Cut 4's input capacitor
|
||||
// closes this.
|
||||
if(_data.SingleAmmoBurst && (!Entity.TrySpendCapacitorCharge(Energy) || !UseAmmo())) return;
|
||||
|
||||
_burstRemaining = (int) BurstCount;
|
||||
_burstInterval = BurstTime / _burstRemaining;
|
||||
|
|
@ -175,7 +178,9 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior
|
|||
{
|
||||
// If multiple ammo is consumed per burst, perform ammo and energy consumption here
|
||||
// UseAmmo returns false when triggering reload; cancel firing if that is the case
|
||||
if (!_data.SingleAmmoBurst && (!Entity.TryConsumeEnergy(Energy) || !UseAmmo()))
|
||||
// Cut 3 (docs/stats-and-power-cut.md): one of the four instant draws out of scope this cut (a shot);
|
||||
// see Trigger()'s SingleAmmoBurst branch above for the same named exception.
|
||||
if (!_data.SingleAmmoBurst && (!Entity.TrySpendCapacitorCharge(Energy) || !UseAmmo()))
|
||||
{
|
||||
_burstRemaining = 0;
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -39,15 +39,15 @@ public class RadiatorData : BehaviorData
|
|||
}
|
||||
}
|
||||
|
||||
public class Radiator : Behavior, IAlwaysUpdatedBehavior, IInitializableBehavior
|
||||
public class Radiator : Behavior, IAlwaysUpdatedBehavior, IInitializableBehavior, IPowerConsumer
|
||||
{
|
||||
public float RadiatorTemperature { get; private set; }
|
||||
|
||||
|
||||
public float Emissivity { get; private set; }
|
||||
public float PumpedHeat { get; private set; }
|
||||
public float WasteHeat { get; private set; }
|
||||
public float EnergyUsage { get; private set; }
|
||||
|
||||
|
||||
private RadiatorData _data;
|
||||
|
||||
public Radiator(RadiatorData data, EquippedItem item) : base(data, item)
|
||||
|
|
@ -59,6 +59,17 @@ public class Radiator : Behavior, IAlwaysUpdatedBehavior, IInitializableBehavior
|
|||
_data = data;
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md): mirrors Execute's own early-out below -- a radiator that would not
|
||||
// even try to pump this tick (waste would outrun pump capacity) requests nothing, exactly like before.
|
||||
public float PowerRequest(float dt)
|
||||
{
|
||||
var pumpedHeat = Evaluate(_data.PumpedHeat);
|
||||
var wasteHeat = Evaluate(_data.WasteHeat);
|
||||
var tempRatio = max(RadiatorTemperature / Temperature, 1);
|
||||
if (tempRatio > pumpedHeat / wasteHeat) return 0f;
|
||||
return Evaluate(_data.EnergyUsage) * tempRatio * dt;
|
||||
}
|
||||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
PumpedHeat = Evaluate(_data.PumpedHeat);
|
||||
|
|
@ -67,12 +78,12 @@ public class Radiator : Behavior, IAlwaysUpdatedBehavior, IInitializableBehavior
|
|||
|
||||
var itemTemperature = Temperature;
|
||||
var tempRatio = max(RadiatorTemperature / itemTemperature, 1);
|
||||
|
||||
|
||||
// Temperature ratio would cause more waste heat than pump capacity, stop executing
|
||||
if (tempRatio > PumpedHeat / WasteHeat) return true;
|
||||
|
||||
if (!Entity.TryConsumeEnergy(EnergyUsage * tempRatio * dt)) return false;
|
||||
|
||||
if (Item != null && Item.PowerSupply < 1f) return false;
|
||||
|
||||
var pumpedHeat = PumpedHeat * max(itemTemperature - _data.TemperatureFloor, 0);
|
||||
|
||||
// Radiator temperature is below temperature floor, stop executing
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@
|
|||
* 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using UniRx;
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
|
|
@ -37,118 +34,68 @@ public class ReactorData : BehaviorData
|
|||
}
|
||||
}
|
||||
|
||||
public class Reactor : Behavior, IOrderedBehavior, IDisposable
|
||||
public class Reactor : Behavior, IOrderedBehavior
|
||||
{
|
||||
private ReactorData _data;
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md §1.2): reported total, not a sink. Nothing writes into this from
|
||||
// outside any more -- PowerBus.NetDraw is the only input Execute reads to decide overload/throttle.
|
||||
public float Draw { get; private set; }
|
||||
|
||||
|
||||
public float CurrentLoadRatio { get; private set; }
|
||||
|
||||
public int Order => 100;
|
||||
|
||||
private List<Capacitor> _capacitors;
|
||||
|
||||
private List<IDisposable> _subscriptions = new List<IDisposable>();
|
||||
|
||||
public Reactor(ReactorData data, EquippedItem item) : base(data, item)
|
||||
{
|
||||
_data = data;
|
||||
FindCapacitors();
|
||||
}
|
||||
public Reactor(ReactorData data, ConsumableItemEffect item) : base(data, item)
|
||||
{
|
||||
_data = data;
|
||||
FindCapacitors();
|
||||
}
|
||||
|
||||
private void FindCapacitors()
|
||||
{
|
||||
_capacitors = Entity.GetBehaviors<Capacitor>().ToList();
|
||||
_subscriptions.Add(Entity.Equipment.ObserveAdd().Subscribe(onAdd =>
|
||||
{
|
||||
var capacitor = onAdd.Value.GetBehavior<Capacitor>();
|
||||
if (capacitor != null) _capacitors.Add(capacitor);
|
||||
}));
|
||||
_subscriptions.Add(Entity.Equipment.ObserveRemove().Subscribe(onRemove =>
|
||||
{
|
||||
var capacitor = onRemove.Value.GetBehavior<Capacitor>();
|
||||
if (capacitor != null) _capacitors.Remove(capacitor);
|
||||
}));
|
||||
}
|
||||
|
||||
public void ConsumeEnergy(float energy)
|
||||
{
|
||||
Draw += energy;
|
||||
}
|
||||
// Cut 3: the resolved generation PowerBus.Step needs before this behaviour's own Execute runs this tick.
|
||||
public float Generation(float dt) => Evaluate(_data.Charge) * dt;
|
||||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
var charge = Evaluate(_data.Charge) * dt;
|
||||
var charge = Generation(dt);
|
||||
var efficiency = Evaluate(_data.Efficiency);
|
||||
|
||||
// This behavior executes last, so any components drawing power have already done so
|
||||
|
||||
// Subtract the baseline charge from draw
|
||||
Draw -= charge;
|
||||
|
||||
// Generate heat using baseline efficiency
|
||||
var heat = charge / efficiency;
|
||||
|
||||
// We have an energy deficit, have to overload the reactor
|
||||
if (Draw > .01f)
|
||||
// Cut 3: PowerBus already decided, for the whole entity, how much demand generation and stored charge
|
||||
// could not cover (positive) or how much generation was left over after demand and after topping up
|
||||
// every capacitor (negative). Split evenly across every online reactor, the same way
|
||||
// Entity.TryConsumeEnergy used to divide unmet demand among reactors before this behaviour ever saw it.
|
||||
var onlineReactors = Entity.GetBehaviors<Reactor>().Count(r => r.Item.Online.Value);
|
||||
var share = onlineReactors > 0 ? Entity.PowerBus.NetDraw / onlineReactors : 0f;
|
||||
|
||||
if (share > .01f)
|
||||
{
|
||||
CurrentLoadRatio = (Draw + charge) / max(charge, .01f);
|
||||
// Deficit: the bus already drained every capacitor it could; this is what is left. Overload power
|
||||
// always neutralizes it, at overload efficiency.
|
||||
CurrentLoadRatio = (share + charge) / max(charge, .01f);
|
||||
var overloadEfficiency = Evaluate(_data.OverloadEfficiency);
|
||||
|
||||
// Generate heat using overload efficiency, usually much less efficient!
|
||||
heat += Draw / overloadEfficiency;
|
||||
|
||||
// Overload power will always neutralize the energy deficit
|
||||
Draw = 0;
|
||||
heat += share / overloadEfficiency;
|
||||
Draw = share;
|
||||
}
|
||||
|
||||
// We have an energy surplus, try to store energy in our capacitors
|
||||
if (Draw < -.01f)
|
||||
else if (share < -.01f)
|
||||
{
|
||||
int nonFullCapacitorCount;
|
||||
do
|
||||
{
|
||||
var chargeToAdd = -Draw;
|
||||
nonFullCapacitorCount = _capacitors.Count(c => c.Charge < c.Capacity - .01f);
|
||||
foreach (var capacitor in _capacitors)
|
||||
{
|
||||
if (capacitor.Charge < capacitor.Capacity - .01f)
|
||||
{
|
||||
var chargeAdded = min(chargeToAdd / nonFullCapacitorCount, capacitor.Capacity - capacitor.Charge);
|
||||
capacitor.AddCharge(chargeAdded);
|
||||
Draw += chargeAdded;
|
||||
}
|
||||
}
|
||||
} while (nonFullCapacitorCount > 0 && Draw < -.01f);
|
||||
}
|
||||
|
||||
// We still have an energy surplus, try to throttle the reactor to reduce heat generation
|
||||
if (Draw < -.01f)
|
||||
{
|
||||
CurrentLoadRatio = (Draw + charge) / max(charge, .01f);
|
||||
heat -= Draw / efficiency * (1 - 1 / Evaluate(_data.ThrottlingFactor));
|
||||
// Surplus the bus could not absorb into any capacitor (all full): throttle to reduce heat generation.
|
||||
CurrentLoadRatio = (share + charge) / max(charge, .01f);
|
||||
heat -= share / efficiency * (1 - 1 / Evaluate(_data.ThrottlingFactor));
|
||||
Draw = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentLoadRatio = 1;
|
||||
Draw = 0;
|
||||
}
|
||||
|
||||
|
||||
Item.SetAudioParameter(SpecialAudioParameter.Intensity, max(.25f, 1 - 1 / CurrentLoadRatio));
|
||||
|
||||
|
||||
AddHeat(heat);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach(var sub in _subscriptions)
|
||||
sub.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -80,9 +80,11 @@ public class Sensor : Behavior, IEventBehavior
|
|||
OnPingEnd = null;
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md): one of the four instant draws out of scope for the bus this cut (a
|
||||
// ping) -- named, temporary exception, spending capacitor charge directly. Cut 4 closes this.
|
||||
public void Ping()
|
||||
{
|
||||
if(_pingCooldown < 0 && Entity.TryConsumeEnergy(Evaluate(_data.PingEnergy)))
|
||||
if(_pingCooldown < 0 && Entity.TrySpendCapacitorCharge(Evaluate(_data.PingEnergy)))
|
||||
{
|
||||
Entity.VisibilitySources[this] = Evaluate(_data.PingVisibility);
|
||||
_pinging = true;
|
||||
|
|
|
|||
|
|
@ -49,14 +49,17 @@ public class Shield : Behavior, IProgressBehavior
|
|||
return true;
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md): one of the four instant draws out of scope for the bus this cut (a
|
||||
// hit taken, not a chosen activation) -- named, temporary exception, spending capacitor charge directly
|
||||
// instead of through PowerBus. Cut 4 gives it a real continuous reserve model and closes this.
|
||||
public bool CanTakeHit(DamageType type, float damage)
|
||||
{
|
||||
return Entity.CanConsumeEnergy(damage * EnergyUsage);
|
||||
return Entity.CanSpendCapacitorCharge(damage * EnergyUsage);
|
||||
}
|
||||
|
||||
public void TakeHit(DamageType type, float damage)
|
||||
{
|
||||
Entity.TryConsumeEnergy(damage * EnergyUsage);
|
||||
Entity.TrySpendCapacitorCharge(damage * EnergyUsage);
|
||||
AddHeat(damage / Efficiency);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public class ThrusterData : BehaviorData
|
|||
}
|
||||
}
|
||||
|
||||
public class Thruster : Behavior, IAnalogBehavior
|
||||
public class Thruster : Behavior, IAnalogBehavior, IPowerConsumer
|
||||
{
|
||||
public float Thrust { get; private set; }
|
||||
public float Torque { get; }
|
||||
|
|
@ -71,10 +71,15 @@ public class Thruster : Behavior, IAnalogBehavior
|
|||
Thrust = Evaluate(_data.Thrust);
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md): the resolved stat times the behaviour-supplied throttle scalar (§1.2),
|
||||
// exactly the shape the map names. _input is set externally (the ship's controls) before Entity.Update calls
|
||||
// PowerBus.Step, so it is already current when this runs.
|
||||
public float PowerRequest(float dt) => _input > .01f ? _input * Evaluate(_data.EnergyUsage) : 0f;
|
||||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
Item.SetAudioParameter(SpecialAudioParameter.Intensity, _input);
|
||||
if(_input > .01f && Entity.TryConsumeEnergy(_input * Evaluate(_data.EnergyUsage)))
|
||||
if(_input > .01f && Item.PowerSupply >= 1f)
|
||||
{
|
||||
Thrust = Evaluate(_data.Thrust);
|
||||
Entity.Velocity -= Direction.xz * _input * Thrust / Entity.Mass * dt;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ public abstract class Entity
|
|||
// so it is collected with the entity instead of surviving it for the life of the process (§0.3, §1.1).
|
||||
public readonly StatResolver Resolver = new StatResolver();
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md): the sole owner of every power grant for this entity, same lifecycle
|
||||
// reasoning as Resolver above (§1.2).
|
||||
public readonly PowerBus PowerBus;
|
||||
|
||||
public EquippedItem[,] GearOccupancy;
|
||||
public HardpointData[,] Hardpoints;
|
||||
public float[,] Armor;
|
||||
|
|
@ -75,7 +79,6 @@ public abstract class Entity
|
|||
private EquippedItem[] _orderedEquipment;
|
||||
private List<Weapon> _weapons = new List<Weapon>();
|
||||
private List<Capacitor> _capacitors = new List<Capacitor>();
|
||||
private List<Reactor> _reactors = new List<Reactor>();
|
||||
private List<Radiator> _heatsinks = new List<Radiator>();
|
||||
|
||||
private List<ConsumableItemEffect> _activeConsumables = new List<ConsumableItemEffect>();
|
||||
|
|
@ -349,6 +352,7 @@ public abstract class Entity
|
|||
Hull = hull;
|
||||
HullData = itemManager.GetData(hull) as HullData;
|
||||
Name = HullData.Name;
|
||||
PowerBus = new PowerBus(this);
|
||||
MapEntity();
|
||||
WeaponGroups = new (List<Weapon> weapons, List<EquippedItem> items)[itemManager.GameplaySettings.WeaponGroupCount];
|
||||
for(int i=0; i<itemManager.GameplaySettings.WeaponGroupCount; i++)
|
||||
|
|
@ -608,8 +612,6 @@ public abstract class Entity
|
|||
}
|
||||
if (b is Capacitor capacitor)
|
||||
_capacitors.Remove(capacitor);
|
||||
if (b is Reactor reactor)
|
||||
_reactors.Remove(reactor);
|
||||
if (b is Radiator heatsink)
|
||||
_heatsinks.Remove(heatsink);
|
||||
if (b is Shield)
|
||||
|
|
@ -793,8 +795,6 @@ public abstract class Entity
|
|||
_weapons.Add(weapon);
|
||||
if(b is Capacitor capacitor)
|
||||
_capacitors.Add(capacitor);
|
||||
if(b is Reactor reactor)
|
||||
_reactors.Add(reactor);
|
||||
if(b is Radiator heatsink)
|
||||
_heatsinks.Add(heatsink);
|
||||
if (b is Shield shield)
|
||||
|
|
@ -858,44 +858,36 @@ public abstract class Entity
|
|||
return true;
|
||||
}
|
||||
|
||||
public bool CanConsumeEnergy(float energy)
|
||||
{
|
||||
var capEnergy = _capacitors.Sum(cap => cap.Charge);
|
||||
int onlineReactors = _reactors.Count(reactor=>reactor.Item.Online.Value);
|
||||
return capEnergy > energy || onlineReactors > 0;
|
||||
}
|
||||
// Cut 3 (docs/stats-and-power-cut.md): replaces CanConsumeEnergy/TryConsumeEnergy for the four instant draws
|
||||
// the cut map names as out of scope for the bus this cut (a burst, a shot, a ping, a hit taken) -- the named,
|
||||
// temporary exception the map accepts, closed by Cut 4's input capacitors. Capacitor charge only: a reactor
|
||||
// no longer bails out a draw it cannot cover for free, which is exactly the behaviour Cut 3 exists to remove
|
||||
// (§0.5 -- "every draw succeeds while one reactor is online"). Atomic: either every capacitor together holds
|
||||
// enough charge and it is spent, or nothing moves -- never TryConsumeEnergy's old partial drain-then-refuse.
|
||||
public bool CanSpendCapacitorCharge(float energy) => _capacitors.Sum(cap => cap.Charge) >= energy;
|
||||
|
||||
public bool TryConsumeEnergy(float energy)
|
||||
public bool TrySpendCapacitorCharge(float energy)
|
||||
{
|
||||
if (energy < .01f) return true;
|
||||
if (!CanSpendCapacitorCharge(energy)) return false;
|
||||
|
||||
var remaining = energy;
|
||||
int chargedCapacitors;
|
||||
do
|
||||
{
|
||||
chargedCapacitors = _capacitors.Count(capacitor => capacitor.Charge > .01f);
|
||||
var chargeToRemove = energy;
|
||||
if (chargedCapacitors == 0) break;
|
||||
var share = remaining / chargedCapacitors;
|
||||
foreach (var cap in _capacitors)
|
||||
{
|
||||
if(cap.Charge > 0.01f)
|
||||
{
|
||||
var chargeRemoved = min(chargeToRemove / chargedCapacitors, cap.Charge);
|
||||
cap.AddCharge(-chargeRemoved);
|
||||
energy -= chargeRemoved;
|
||||
}
|
||||
if (cap.Charge <= .01f) continue;
|
||||
var drawn = min(share, cap.Charge);
|
||||
cap.AddCharge(-drawn);
|
||||
remaining -= drawn;
|
||||
}
|
||||
} while (chargedCapacitors > 0 && energy > .01f);
|
||||
} while (chargedCapacitors > 0 && remaining > .01f);
|
||||
|
||||
if (energy < .01f) return true;
|
||||
|
||||
int onlineReactors = _reactors.Count(reactor=>reactor.Item.Online.Value);
|
||||
foreach (var reactor in _reactors)
|
||||
{
|
||||
if (reactor.Item.Online.Value)
|
||||
{
|
||||
reactor.ConsumeEnergy(energy / onlineReactors);
|
||||
}
|
||||
}
|
||||
|
||||
return onlineReactors > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AddChild(Entity entity)
|
||||
|
|
@ -1039,6 +1031,10 @@ public abstract class Entity
|
|||
}
|
||||
}
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md §1.2): stepped once per tick, before any equipped item's
|
||||
// Behaviors execute, so every IPowerConsumer's grant is decided before it acts on it.
|
||||
PowerBus.Step(delta);
|
||||
|
||||
foreach (var equippedItem in _orderedEquipment)
|
||||
{
|
||||
equippedItem.Update(delta);
|
||||
|
|
@ -1390,6 +1386,13 @@ public class EquippedItem : IStatContext
|
|||
public float ConsumableProgressFactor(float exponent) => 1f;
|
||||
public float PowerSupplyFactor(float exponent) => 1f;
|
||||
|
||||
// Cut 3 (docs/stats-and-power-cut.md §1.2): PowerBus's grant ratio for this item, in [0,1], from whichever
|
||||
// tick last ran its Step. Display-only and (once Cut 6 wires StatSource.PowerSupply into the resolver)
|
||||
// stat-source-only: the only other reader is the very IPowerConsumer behaviour that declared the request this
|
||||
// reflects. Forbidden writers: nothing but PowerBus sets this. Defaults to 1 (fully supplied) for an item
|
||||
// that draws no power, or before the bus has run once.
|
||||
public float PowerSupply { get; internal set; } = 1f;
|
||||
|
||||
// Cut 2: these used to read the catalog stat's own per-entity dictionary (the leak, §0.3). A modifier now
|
||||
// attaches to this entity's resolver, keyed by (this item, stat); reading it here is unchanged.
|
||||
public float ScaleModifier(PerformanceStat stat) => Entity.Resolver.ScaleModifier(this, stat);
|
||||
|
|
|
|||
143
Assets/Scripts/ServerShared/PowerBus.cs
Normal file
143
Assets/Scripts/ServerShared/PowerBus.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue