Paints carry search or track; a ping sweep warns every ship it crosses in its beam; allies hear the return silently
LockWarning becomes PaintWarning with a PaintMode (Search for a ping sweep, Track for a lock); Entity.IncomingLocks becomes IncomingPaints. Sensor replaces the _pingedEntities set with a stateless crossing rule over the previous and current wavefront radius, limited to a new BeamWidth stat (key 12, absent reads 360), and credits allied pings from the receiving sensor's own sensitivity and distance. The HUD lock indicator follows Track paints only. Folds in the radiator sampling-instant pin. Unverified at this commit (first build still to run). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a47f967d13
commit
c47ced9cfd
8 changed files with 432 additions and 62 deletions
|
|
@ -102,7 +102,7 @@ public class LockWeapon : InstantWeapon
|
|||
{
|
||||
var lerp = 1 - unlerp(0, 90, degrees);
|
||||
_lock = saturate(_lock + pow(lerp, DirectionImpact) * dt * LockSpeed * pow(Entity.EntityInfoGathered[target], SensorImpact));
|
||||
target.ReceivePaint(Entity.Position, Item?.Data, Lock);
|
||||
target.ReceivePaint(Entity.Position, Item?.Data, PaintMode.Track, Lock);
|
||||
}
|
||||
else _lock = saturate(_lock - dt * Decay);
|
||||
}
|
||||
|
|
@ -110,19 +110,3 @@ public class LockWeapon : InstantWeapon
|
|||
return base.Execute(dt);
|
||||
}
|
||||
}
|
||||
|
||||
// What a painted ship senses of one beam on it (ruling sensor-stat-set item 4; strength per ruling
|
||||
// lock-warning-strength). Strength is the emitter's own lock progress at the paint, 0..1, never summed.
|
||||
public readonly struct LockWarning
|
||||
{
|
||||
public readonly float2 Bearing;
|
||||
public readonly EquippableItemData EmitterClass;
|
||||
public readonly float Strength;
|
||||
|
||||
public LockWarning(float2 bearing, EquippableItemData emitterClass, float strength)
|
||||
{
|
||||
Bearing = bearing;
|
||||
EmitterClass = emitterClass;
|
||||
Strength = strength;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ public class SensorData : BehaviorData
|
|||
|
||||
[Inspectable, JsonProperty("pingRadiusExponent"), Key(11)]
|
||||
public float PingRadiusExponent = .5f;
|
||||
|
||||
// The width in degrees of the cone a ping sweeps, about the sensor's facing (ruling illuminator-beamforming:
|
||||
// how much a ping reveals is a stat). 360 or more is omnidirectional; a design without it reads as 360.
|
||||
[Inspectable, JsonProperty("beamWidth"), Key(12)]
|
||||
public PerformanceStat BeamWidth;
|
||||
|
||||
public override Behavior CreateInstance(EquippedItem item)
|
||||
{
|
||||
|
|
@ -61,7 +66,10 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
private float _pingLerp;
|
||||
private bool _pinging;
|
||||
private float _pingRadius;
|
||||
private HashSet<Entity> _pingedEntities = new HashSet<Entity>();
|
||||
private float _previousRadius;
|
||||
|
||||
// Allied sensors mid-sweep this tick, gathered once per Execute (scratch, rebuilt every call).
|
||||
private readonly List<Sensor> _alliedPingers = new List<Sensor>();
|
||||
|
||||
public float Cooldown
|
||||
{
|
||||
|
|
@ -73,6 +81,24 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
get => _pingRadius;
|
||||
}
|
||||
|
||||
// The wavefront radius before this tick's step: the ping crossed everything in [PreviousRadius, PingRadius).
|
||||
public float PreviousRadius => _previousRadius;
|
||||
|
||||
public float BeamWidth => _data.BeamWidth == null ? 360f : Evaluate(_data.BeamWidth);
|
||||
|
||||
public float2 Facing => Direction.xz;
|
||||
|
||||
// The one crossing rule: this tick's ping wavefront of `pinger` passed `subject`, inside the pinger's beam.
|
||||
// It remembers nothing; the two radii are the whole state, so every observer of the same ping agrees.
|
||||
private static bool Crossing(Sensor pinger, Entity subject)
|
||||
{
|
||||
var diff = subject.Position.xz - pinger.Entity.Position.xz;
|
||||
var dist = length(diff);
|
||||
if (dist < pinger._previousRadius || dist >= pinger._pingRadius) return false;
|
||||
var beam = pinger.BeamWidth;
|
||||
return beam >= 360f || dot(pinger.Facing, diff) >= dist * cos(beam * .5f * .0174533f);
|
||||
}
|
||||
|
||||
public float PingBrightness => pow(1 - _pingLerp, _data.PingRadiusExponent);
|
||||
|
||||
public event Action OnPingStart;
|
||||
|
|
@ -119,7 +145,6 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
_pingCooldown = 1;
|
||||
_pingLerp = 0;
|
||||
_pingRadius = 0;
|
||||
_pingedEntities.Clear();
|
||||
OnPingStart?.Invoke();
|
||||
}
|
||||
}
|
||||
|
|
@ -140,6 +165,7 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
// instance never reaches PowerBus.Step (see Ping()) and so never accrues charge.
|
||||
if (Item != null)
|
||||
_capacitor.AddCharge(_capacitor.RequestedFill(dt) * Item.PowerSupply);
|
||||
_previousRadius = _pingRadius;
|
||||
if (_pinging)
|
||||
{
|
||||
_pingLerp += dt / _data.PingDuration;
|
||||
|
|
@ -154,6 +180,13 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
_pingCooldown -= dt / Evaluate(_data.PingCooldown);
|
||||
|
||||
var forward = Direction.xz;
|
||||
_alliedPingers.Clear();
|
||||
foreach (var ally in Entity.Zone.Entities)
|
||||
{
|
||||
if (ally == Entity || ally.Faction == null || ally.Faction != Entity.Faction || Entity.IsHostileTo(ally)) continue;
|
||||
foreach (var sensor in ally.GetBehaviors<Sensor>())
|
||||
if (sensor._previousRadius < sensor._pingRadius) _alliedPingers.Add(sensor);
|
||||
}
|
||||
foreach (var entity in Entity.Zone.Entities)
|
||||
{
|
||||
if (entity == Entity) continue;
|
||||
|
|
@ -163,10 +196,22 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
var obscuration = Entity.Zone.Obscuration(Entity.Position.xz, entity.Position.xz);
|
||||
float previous, next;
|
||||
Entity.EntityInfoGathered.TryGetValue(entity, out previous);
|
||||
if (!_pingedEntities.Contains(entity) && dist < _pingRadius)
|
||||
|
||||
// A crossing of this entity's own ping paints the subject (it learns it is swept); a crossing of an
|
||||
// allied ping is heard silently (ruling sensor-stat-set item 3): credit, no paint, no emission.
|
||||
var crossings = 0;
|
||||
if (Crossing(this, entity))
|
||||
{
|
||||
_pingedEntities.Add(entity);
|
||||
next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), Evaluate(_data.PingBoost), dist, dt, true, obscuration));
|
||||
crossings++;
|
||||
entity.ReceivePaint(Entity.Position, Item?.Data, PaintMode.Search, 0);
|
||||
}
|
||||
foreach (var pinger in _alliedPingers)
|
||||
if (pinger.Entity != entity && Crossing(pinger, entity)) crossings++;
|
||||
|
||||
if (crossings > 0)
|
||||
{
|
||||
var response = _data.PingBoost == null ? 0f : Evaluate(_data.PingBoost);
|
||||
next = saturate(previous + crossings * Gain(entity.Visibility, Evaluate(_data.Sensitivity), response, dist, dt, true, obscuration));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -185,12 +185,13 @@ public abstract class Entity
|
|||
public Subject<Unit> HypothermiaDeath = new Subject<Unit>();
|
||||
public Subject<Entity> TargetedBy = new Subject<Entity>();
|
||||
public ReactiveProperty<int> TargetedByCount = new ReactiveProperty<int>(0);
|
||||
// TargetedBy counts selection, which emits nothing; a lock warning comes only from a paint. LockWeapon.Execute
|
||||
// reports each tick it paints this ship into the inbox; Update publishes the inbox once, so a warning lives
|
||||
// exactly one tick after its last paint.
|
||||
private readonly List<LockWarning> _paintInbox = new List<LockWarning>();
|
||||
private readonly List<LockWarning> _incomingLocks = new List<LockWarning>();
|
||||
public IReadOnlyList<LockWarning> IncomingLocks => _incomingLocks;
|
||||
// TargetedBy counts selection, which emits nothing; a warning comes only from a paint. A search paint is a ping
|
||||
// sweeping this ship (Sensor.Execute); a track paint is a lock (LockWeapon.Execute). Each reports into the
|
||||
// inbox the tick it paints; PublishPaints publishes the inbox once, so a warning lives exactly one tick after
|
||||
// its last paint.
|
||||
private readonly List<PaintWarning> _paintInbox = new List<PaintWarning>();
|
||||
private readonly List<PaintWarning> _incomingPaints = new List<PaintWarning>();
|
||||
public IReadOnlyList<PaintWarning> IncomingPaints => _incomingPaints;
|
||||
public ReactiveProperty<SecurityLevel> CurrentSecurityLevel = new ReactiveProperty<SecurityLevel>(SecurityLevel.Open);
|
||||
public ReadOnlyReactiveProperty<bool> PresencePermitted;
|
||||
|
||||
|
|
@ -802,7 +803,7 @@ public abstract class Entity
|
|||
VisibleEnemies.Clear();
|
||||
VisibleFriendlies.Clear();
|
||||
_paintInbox.Clear();
|
||||
_incomingLocks.Clear();
|
||||
_incomingPaints.Clear();
|
||||
}
|
||||
|
||||
// Zone.Update runs this on every entity before any entity updates, so a paint made during tick N is published
|
||||
|
|
@ -810,16 +811,17 @@ public abstract class Entity
|
|||
// one tick later for every ordering.
|
||||
internal void PublishPaints()
|
||||
{
|
||||
_incomingLocks.Clear();
|
||||
_incomingLocks.AddRange(_paintInbox);
|
||||
_incomingPaints.Clear();
|
||||
_incomingPaints.AddRange(_paintInbox);
|
||||
_paintInbox.Clear();
|
||||
}
|
||||
|
||||
// The only caller is LockWeapon.Execute; strength is the emitter's own Lock at that paint.
|
||||
internal void ReceivePaint(float3 emitterPosition, EquippableItemData emitterClass, float strength)
|
||||
// The callers are Sensor.Execute (a Search paint, Strength 0) and LockWeapon.Execute (a Track paint, Strength
|
||||
// the emitter's own Lock at that paint).
|
||||
internal void ReceivePaint(float3 emitterPosition, EquippableItemData emitterClass, PaintMode mode, float strength)
|
||||
{
|
||||
if (!_active) return;
|
||||
_paintInbox.Add(new LockWarning(normalize((emitterPosition - Position).xz), emitterClass, strength));
|
||||
_paintInbox.Add(new PaintWarning(normalize((emitterPosition - Position).xz), emitterClass, mode, strength));
|
||||
}
|
||||
|
||||
public Entity(ItemManager itemManager, Zone zone, EquippableItem hull, EntitySettings settings)
|
||||
|
|
|
|||
|
|
@ -28,3 +28,28 @@ public readonly struct Signature
|
|||
|
||||
public Signature WithStrength(float strength) => new Signature(Channel, strength, Item);
|
||||
}
|
||||
|
||||
// What a painted ship learns of a beam on it (ruling sensor-stat-set item 4): a Search paint is a ping sweeping
|
||||
// it (Strength 0); a Track paint is a lock (Strength is the emitter's own lock progress, 0..1, ruling
|
||||
// lock-warning-strength). Never summed.
|
||||
public enum PaintMode
|
||||
{
|
||||
Search,
|
||||
Track
|
||||
}
|
||||
|
||||
public readonly struct PaintWarning
|
||||
{
|
||||
public readonly float2 Bearing;
|
||||
public readonly EquippableItemData EmitterClass;
|
||||
public readonly PaintMode Mode;
|
||||
public readonly float Strength;
|
||||
|
||||
public PaintWarning(float2 bearing, EquippableItemData emitterClass, PaintMode mode, float strength)
|
||||
{
|
||||
Bearing = bearing;
|
||||
EmitterClass = emitterClass;
|
||||
Mode = mode;
|
||||
Strength = strength;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue