Merge origin/master (da6fe033) into faction-play-1
Combat.cs: master's lazy optimum-range owner kept, the OptimumRange accessor beside it. FireControlPerWeaponTests: the exponent test runs the first Update before reading the optimum (master derives it there). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
2ff48d0b45
10 changed files with 800 additions and 12 deletions
|
|
@ -10,22 +10,42 @@ using float2x2 = CultMath.float2x2;
|
|||
public class CombatState : BaseState
|
||||
{
|
||||
private const int DPS_SAMPLE_COUNT = 32;
|
||||
// The Strength of one Track paint in this ship's own IncomingPaints: a fire-control emitter tracking this ship
|
||||
// that hard (rulings lock-warning-strength, sensor-stat-set item 4, launcher-lock-solution-quality).
|
||||
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;
|
||||
public float OptimumRange => _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));
|
||||
_agent.Ship.Equipment.ObserveRemove().Subscribe(_ => _optimumStale = true);
|
||||
}
|
||||
|
||||
// The optimum is a function of the loadout's evaluated stats, so it is re-derived at the next Update (agents update
|
||||
// before the entities, so by then the weapons that came online this tick have evaluated) whenever a weapon is
|
||||
// added, removed or changes online state. Sampling on the event itself would read the stats before they exist: a
|
||||
// ship the zone just spawned would keep an optimum of 0.
|
||||
private void Watch(EquippedItem item)
|
||||
{
|
||||
if (item.Behaviors.OfType<Weapon>().Any()) item.Online.Subscribe(_ => _optimumStale = true);
|
||||
}
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
if (_optimumStale)
|
||||
{
|
||||
SampleDps();
|
||||
_optimumStale = false;
|
||||
}
|
||||
|
||||
var target = _agent.Ship.Target.Value.Entity;
|
||||
if (target == null) return;
|
||||
|
||||
|
|
@ -79,6 +99,8 @@ public class CombatState : BaseState
|
|||
|
||||
foreach(var w in _availableLockingWeapons) w.Activate();
|
||||
|
||||
UseConsumables(toTarget, targetDistance);
|
||||
|
||||
var selectedGroup = -1;
|
||||
var maxDps = Single.MinValue;
|
||||
foreach (var group in _availableGroups)
|
||||
|
|
@ -133,6 +155,27 @@ public class CombatState : BaseState
|
|||
// Fire charged guns!
|
||||
}
|
||||
|
||||
// The AI plays the player's rule on the player's information: vent when one Track paint on this ship passes half,
|
||||
// otherwise overdrive to close a distant target ahead. It reads only this ship's own IncomingPaints. Both go
|
||||
// through TryActivateConsumable, and a ship carrying neither does nothing.
|
||||
private void UseConsumables(float3 toTarget, float targetDistance)
|
||||
{
|
||||
var ship = _agent.Ship;
|
||||
if (ship.IncomingPaints.Any(p => p.Mode == PaintMode.Track && p.Strength > 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);
|
||||
}
|
||||
|
||||
// One of a kind at a time: a Stackable design would otherwise be drained once per tick.
|
||||
private void Use(Predicate<BehaviorData> kind)
|
||||
{
|
||||
if (_agent.Ship.HasActiveConsumable(kind)) return;
|
||||
var consumable = _agent.Ship.FirstCarriedConsumable(kind);
|
||||
if (consumable != null) _agent.Ship.TryActivateConsumable(consumable);
|
||||
}
|
||||
|
||||
private void SampleDps()
|
||||
{
|
||||
var minRange = Single.MaxValue;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,29 @@ public static class ColorMath
|
|||
return saturate(float3(r,g,b));
|
||||
}
|
||||
|
||||
// A hue band on the circle runs from start up to end, wrapping past 1 when end < start.
|
||||
public static bool HueInBand(float hue, float start, float end) => frac(hue - start) < frac(end - start);
|
||||
|
||||
// Maps u in [0,1) onto the hues outside the band, so a uniform draw stays uniform over the allowed arc.
|
||||
public static float HueOutsideBand(float u, float start, float end) => frac(end + u * AllowedArc(start, end));
|
||||
|
||||
static float AllowedArc(float start, float end) => 1 - frac(end - start);
|
||||
|
||||
// The second hue of a pair whose first is HueOutsideBand(u): distance away along the allowed arc, on the side given by up,
|
||||
// reflected to the other side if that would leave the arc. Both hues lie on one arc, so the straight RGB blend between
|
||||
// the two stops never crosses the band. The distance is capped at half the arc, so the reflection always fits and a
|
||||
// wide band shortens the distance instead of leaving the secondary inside it.
|
||||
public static float SecondaryHueOutsideBand(float u, bool up, float distance, float start, float end)
|
||||
{
|
||||
float arc = AllowedArc(start, end);
|
||||
float d = min(distance, arc / 2);
|
||||
float t = u * arc;
|
||||
float s = up ? t + d : t - d;
|
||||
if (s < 0 || s > arc)
|
||||
s = up ? t - d : t + d;
|
||||
return frac(end + s);
|
||||
}
|
||||
|
||||
public static float3 HsvToRgb(in float3 hsv)
|
||||
{
|
||||
float3 rgb = HueToRgb(hsv.x);
|
||||
|
|
@ -35,7 +58,7 @@ public static class ColorMath
|
|||
public static float3 RgbToHcv(in float3 rgb)
|
||||
{
|
||||
// Based on work by Sam Hocevar and Emil Persson
|
||||
float4 p = (rgb.y < rgb.z) ? float4(rgb.zy, -1.0f, 2.0f/3.0f) : float4(rgb.zy, 0.0f, -1.0f/3.0f);
|
||||
float4 p = (rgb.y < rgb.z) ? float4(rgb.zy, -1.0f, 2.0f/3.0f) : float4(rgb.yz, 0.0f, -1.0f/3.0f);
|
||||
float4 q = (rgb.x < p.x) ? float4(p.xyw, rgb.x) : float4(rgb.x, p.yzx);
|
||||
float c = q.x - min(q.w, q.y);
|
||||
float h = abs((q.w - q.y) / (6 * c + Epsilon) + q.z);
|
||||
|
|
|
|||
|
|
@ -1353,8 +1353,20 @@ public abstract class Entity
|
|||
return data.Stackable || FindActiveConsumable(data) == null;
|
||||
}
|
||||
|
||||
// The first consumable design in cargo with a behaviour of the asked kind, or null. Callers ask by kind, never
|
||||
// by name: a design's name is a label and its behaviours are what it does.
|
||||
public ConsumableItemData FirstCarriedConsumable(Predicate<BehaviorData> kind) =>
|
||||
CargoBays.SelectMany(bay => bay.Cargo.Keys).OfType<ConsumableItem>()
|
||||
.Select(item => ItemManager.GetData(item) as ConsumableItemData)
|
||||
.FirstOrDefault(data => data != null && data.Behaviors.Any(b => kind(b)));
|
||||
|
||||
public bool HasActiveConsumable(Predicate<BehaviorData> kind) =>
|
||||
_activeConsumables.Any(e => e.Data.Behaviors.Any(b => kind(b)));
|
||||
|
||||
public bool TryActivateConsumable(ConsumableItemData data)
|
||||
{
|
||||
// Death and docking Remove and Deactivate the ship while its agent keeps ticking.
|
||||
if (!_active) return false;
|
||||
if (!CanActivateConsumable(data)) return false;
|
||||
|
||||
var key = ItemManager.ItemData.RefOf<ItemData>(data).Key;
|
||||
|
|
|
|||
|
|
@ -142,6 +142,9 @@ public class ZoneGenerationSettings
|
|||
public float SunSecondaryColorDistance = .25f;
|
||||
public float SunLightSaturation = .5f;
|
||||
public float SunFogTintSaturation = .5f;
|
||||
// The green band no sun colour may take, as fractions of the hue circle (75 to 165 degrees: chartreuse through spring green).
|
||||
public float SunHueExcludedStart = 75f / 360f;
|
||||
public float SunHueExcludedEnd = 165f / 360f;
|
||||
|
||||
public ExponentialLerp GasGiantBandCount;
|
||||
public float GasGiantBandColorSeparation = .25f;
|
||||
|
|
|
|||
|
|
@ -159,8 +159,10 @@ public static class ZoneGenerator
|
|||
{
|
||||
if (gas is SunData sun)
|
||||
{
|
||||
float primary = random.NextFloat();
|
||||
float secondary = frac(primary + 1 + zoneSettings.SunSecondaryColorDistance * (random.NextFloat() > .5 ? 1 : -1));
|
||||
var hueDraw = random.NextFloat();
|
||||
float primary = ColorMath.HueOutsideBand(hueDraw, zoneSettings.SunHueExcludedStart, zoneSettings.SunHueExcludedEnd);
|
||||
float secondary = ColorMath.SecondaryHueOutsideBand(hueDraw, random.NextFloat() > .5, zoneSettings.SunSecondaryColorDistance,
|
||||
zoneSettings.SunHueExcludedStart, zoneSettings.SunHueExcludedEnd);
|
||||
gas.Colors = new []
|
||||
{
|
||||
float4(ColorMath.HsvToRgb(float3(primary, zoneSettings.SunColorSaturation, .5f)), 0),
|
||||
|
|
|
|||
Loading…
Reference in a new issue