CombatState activates a vapour dump when an enemy lock on the ship passes .5, else a throttle lock when the target is past twice the optimum range and ahead within 15 degrees. Consumables are picked by behaviour kind through Entity.FirstCarriedConsumable and activated through TryActivateConsumable. Work in progress: tests not yet run (this commit may not pass them). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
195 lines
No EOL
8.5 KiB
C#
195 lines
No EOL
8.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UniRx;
|
|
using static CultMath.math;
|
|
using float2 = CultMath.float2;
|
|
using float3 = CultMath.float3;
|
|
using float2x2 = CultMath.float2x2;
|
|
|
|
public class CombatState : BaseState
|
|
{
|
|
private const int DPS_SAMPLE_COUNT = 32;
|
|
// A lock past this fraction is one the target feels (ruling sensor-stat-set: a painted target always learns it).
|
|
private const float LockAlarm = .5f;
|
|
// Overdrive only to close a target farther than this many optimum ranges, while heading within this many degrees of it.
|
|
private const float OverdriveRangeFactor = 2f;
|
|
private const float OverdriveArcDegrees = 15f;
|
|
private float _optimumRange;
|
|
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());
|
|
}
|
|
}
|
|
|
|
public override void Update(float delta)
|
|
{
|
|
var target = _agent.Ship.Target.Value.Entity;
|
|
if (target == null) return;
|
|
|
|
_availableGroups.Clear();
|
|
_availableLockingWeapons.Clear();
|
|
|
|
var toTarget = target.Position - _agent.Ship.Position;
|
|
var targetDistance = length(toTarget);
|
|
|
|
// Cut 2 (docs/fire-control-cut.md): aim at whichever revealed weapon on the target hits hardest at
|
|
// this range, or clear the aim point when nothing qualifies -- the same predicate
|
|
// (FireControl.IsRevealed) and the same writer (TrySelectTargetItem) the player path uses.
|
|
EquippedItem bestTargetItem = null;
|
|
var bestTargetDps = float.MinValue;
|
|
foreach (var targetItem in target.Equipment)
|
|
{
|
|
if (!(targetItem.Behaviors.FirstOrDefault(b => b is Weapon) is Weapon targetWeapon)) continue;
|
|
if (!FireControl.IsRevealed(_agent.Ship, targetItem)) continue;
|
|
var dps = targetWeapon.RangeDamagePerSecond(targetDistance);
|
|
if (dps > bestTargetDps)
|
|
{
|
|
bestTargetDps = dps;
|
|
bestTargetItem = targetItem;
|
|
}
|
|
}
|
|
_agent.Ship.TrySelectTargetItem(bestTargetItem);
|
|
|
|
for (var i = 0; i < _agent.Ship.WeaponGroups.Length; i++)
|
|
{
|
|
var group = _agent.Ship.WeaponGroups[i];
|
|
var dps = 0f;
|
|
foreach (var weapon in group.weapons)
|
|
// The range test is FireControl's (Designated), and a weapon Solve refuses adds nothing to a group's
|
|
// damage.
|
|
if (weapon.Item.Online.Value &&
|
|
FireControl.Designated(weapon, _agent.Ship, target, out _) &&
|
|
!FireControl.Refuses(weapon, _agent.Ship) &&
|
|
(weapon is ConstantWeapon ||
|
|
weapon is InstantWeapon instantWeapon && instantWeapon.CanFire))
|
|
{
|
|
if(weapon is LockWeapon lockWeapon)
|
|
_availableLockingWeapons.Add(lockWeapon);
|
|
else
|
|
dps += weapon.RangeDamagePerSecond(targetDistance);
|
|
}
|
|
|
|
if (dps > .1) _availableGroups.Add((i, dps));
|
|
}
|
|
|
|
foreach(var w in _availableLockingWeapons) w.Activate();
|
|
|
|
UseConsumables(toTarget, targetDistance);
|
|
|
|
var selectedGroup = -1;
|
|
var maxDps = Single.MinValue;
|
|
foreach (var group in _availableGroups)
|
|
{
|
|
if (group.dps > maxDps)
|
|
{
|
|
maxDps = group.dps;
|
|
selectedGroup = group.index;
|
|
}
|
|
}
|
|
|
|
var targetRight = target.Direction.Rotate(ItemRotation.Clockwise);
|
|
var optimumRangeDelta = abs(_optimumRange - targetDistance);
|
|
var directionToTarget = normalize(toTarget.xz);
|
|
var targetPortAlignment = dot(targetRight, directionToTarget);
|
|
var targetForeAlignment = dot(-target.Direction, directionToTarget);
|
|
var forwardness = saturate(optimumRangeDelta / _agent.Settings.AgentMaxForwardDistance) * _agent.Settings.AgentForwardLerp;
|
|
if (targetForeAlignment > 0) forwardness = lerp(forwardness, 1, pow(targetPortAlignment, 2));
|
|
|
|
var movementDirection = normalize(lerp(
|
|
directionToTarget.Rotate(targetPortAlignment > 0 ? ItemRotation.Clockwise : ItemRotation.CounterClockwise),
|
|
targetDistance > _optimumRange ? directionToTarget : -directionToTarget, forwardness));
|
|
|
|
if (selectedGroup >= 0)
|
|
{
|
|
var testWeapon = _agent.Ship.WeaponGroups[selectedGroup].weapons.First();
|
|
if(testWeapon.Velocity > 1)
|
|
{
|
|
// Cut 3 (docs/fire-control-cut.md): the one prediction function, shared with FireControl.Fire's
|
|
// own snapshot and the roll's deviation measurement -- two separate predictions here and in the
|
|
// roll would let an AI aim at one point and get judged for missing a different one.
|
|
var targetHullData = _agent.ItemManager.GetData(target.Hull) as HullData;
|
|
var predictedPosition = FireControl.PredictedIntercept(testWeapon, _agent.Ship, target);
|
|
predictedPosition.y = _agent.Ship.Zone.GetHeight(predictedPosition.xz) + targetHullData.GridOffset;
|
|
toTarget = normalize(predictedPosition - _agent.Ship.Position);
|
|
}
|
|
|
|
// Each weapon decides for itself (operator ruling 2026-09-30): a group shares a trigger, not a
|
|
// verdict. FireControl.AgentFires is the AI's "worth it" heuristic for a weapon that hits what it
|
|
// fires at, and "the target is designated" for a fused one.
|
|
foreach (var weapon in _agent.Ship.WeaponGroups[selectedGroup].weapons)
|
|
{
|
|
if (FireControl.AgentFires(weapon, _agent.Ship, target))
|
|
weapon.Activate();
|
|
else if (weapon.Firing)
|
|
weapon.Deactivate();
|
|
}
|
|
}
|
|
_agent.Ship.Aim = toTarget;
|
|
_agent.Ship.Turn = Steering.Toward(_agent.Ship, toTarget.xz);
|
|
_agent.Accelerate(movementDirection * _agent.TopSpeed, true);//selectedGroup >= 0);
|
|
|
|
// Fire charged guns!
|
|
}
|
|
|
|
// The AI plays the player's rule on the player's information: vent when an enemy's lock on this ship passes half,
|
|
// otherwise overdrive to close a distant target ahead. Both go through TryActivateConsumable, and a ship carrying
|
|
// neither does nothing.
|
|
private void UseConsumables(float3 toTarget, float targetDistance)
|
|
{
|
|
var ship = _agent.Ship;
|
|
if (ship.VisibleEnemies.Any(e => e.Target.Value.Entity == ship &&
|
|
e.Weapons.OfType<LockWeapon>().Any(w => w.Lock > LockAlarm)))
|
|
Use(b => b is VapourDumpData);
|
|
else if (targetDistance > OverdriveRangeFactor * _optimumRange &&
|
|
dot(normalize(ship.Direction), normalize(toTarget.xz)) > cos(radians(OverdriveArcDegrees)))
|
|
Use(b => b is ThrottleLockData);
|
|
}
|
|
|
|
private void Use(Predicate<BehaviorData> kind)
|
|
{
|
|
var consumable = _agent.Ship.FirstCarriedConsumable(kind);
|
|
if (consumable != null) _agent.Ship.TryActivateConsumable(consumable);
|
|
}
|
|
|
|
private void SampleDps()
|
|
{
|
|
var minRange = Single.MaxValue;
|
|
var maxRange = Single.MinValue;
|
|
foreach (var weapon in _agent.Ship.Weapons)
|
|
{
|
|
if (weapon.Range < minRange)
|
|
minRange = weapon.Range;
|
|
if (weapon.Range > maxRange)
|
|
maxRange = weapon.Range;
|
|
}
|
|
|
|
var offset = _agent.ItemManager.Random.NextFloat((maxRange - minRange) / DPS_SAMPLE_COUNT);
|
|
var optimumDPS = 0f;
|
|
for (int i = 0; i < DPS_SAMPLE_COUNT; i++)
|
|
{
|
|
var range = offset + minRange + (maxRange - minRange) * ((float) i / DPS_SAMPLE_COUNT);
|
|
float dps = 0;
|
|
foreach (var weapon in _agent.Ship.Weapons)
|
|
{
|
|
if (weapon.Item.Online.Value && weapon.MinRange < range && range < weapon.Range)
|
|
dps += weapon.RangeDamagePerSecond(range);
|
|
}
|
|
|
|
// Multiply by range raised to a (sublinear) exponent to bias DPS preference towards longer ranges
|
|
dps *= pow(range, _agent.Settings.AgentRangeExponent);
|
|
|
|
if (dps > optimumDPS)
|
|
{
|
|
optimumDPS = dps;
|
|
_optimumRange = range;
|
|
}
|
|
}
|
|
}
|
|
} |