CombatState derives its optimum range once the weapons' stats are known, and re-derives on loadout change

A zone-spawned ship's CombatState sampled in its constructor, before any weapon had evaluated its stats,
so the optimum range read 0 and never moved. It now marks the optimum stale on construction, on any
equipment add/remove and on a weapon's online change, and samples at the next Update once every active
weapon reports StatsKnown. ConsumableAiTests build the agent through Zone.Admit(piloted: true) and read
the optimum without re-sampling; new rows pin the hold scope, hull-heading source, mixed paints, sight
independence and the optimum across two guns and a refit.

Not yet verified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-16 19:03:50 +02:00
commit bf32a82e32
3 changed files with 166 additions and 29 deletions

View file

@ -17,20 +17,33 @@ public class CombatState : BaseState
private const float OverdriveRangeFactor = 2f;
private const float OverdriveArcDegrees = 15f;
private float _optimumRange;
private bool _optimumStale = true;
private readonly List<(int index, float dps)> _availableGroups = new List<(int index, float dps)>();
private readonly List<LockWeapon> _availableLockingWeapons = new List<LockWeapon>();
public CombatState(Agent agent) : base(agent)
{
SampleDps();
foreach (var weapon in _agent.Ship.Weapons)
{
weapon.Item.Online.Subscribe(b => SampleDps());
}
foreach (var item in _agent.Ship.Equipment) Watch(item);
_agent.Ship.Equipment.ObserveAdd().Subscribe(add => { Watch(add.Value); _optimumStale = true; });
_agent.Ship.Equipment.ObserveRemove().Subscribe(_ => _optimumStale = true);
}
// The optimum is a function of the loadout's evaluated stats, so it is re-derived whenever a weapon comes or goes
// or changes online state, and only once every weapon that can run has evaluated its stats (a weapon evaluates on
// its first active tick, after it comes online): a ship the zone just spawned samples over live stats, not zeros.
private void Watch(EquippedItem item)
{
if (item.Behaviors.OfType<Weapon>().Any()) item.Online.Subscribe(_ => _optimumStale = true);
}
public override void Update(float delta)
{
if (_optimumStale && _agent.Ship.Weapons.All(w => w.StatsKnown || !w.Item.Active.Value))
{
SampleDps();
_optimumStale = false;
}
var target = _agent.Ship.Target.Value.Entity;
if (target == null) return;

View file

@ -173,9 +173,13 @@ public abstract class Weapon : Behavior, IActivatedBehavior
Tracking = Evaluate(_data.Tracking);
}
// False until the first active tick has evaluated the stats above: before it they all read zero.
public bool StatsKnown { get; private set; }
public override bool Execute(float dt)
{
UpdateStats();
StatsKnown = true;
return true;
}