Sensor: the pinger's swept set decides each crossing once; allies credit a published ping return inside the bistatic oval
Deletes the stateless interval crossing (_previousRadius, PreviousRadius, Crossing) and the allied-pinger scan over other ships' Sensors. The pinging Sensor remembers the subjects its wavefront has reached (_reached), in the beam or not, and a subject's first sweeping tick inside the radius is its one crossing. On it the pinger paints, credits itself and posts a PingReturn to each ally; Entity publishes returns with paints (PublishPaints is now PublishInbox) and the ally's own sensors credit them for one tick at the geometric mean range, inside Rt x Rr < PingRange^2, through the geometric mean of both legs' obscuration (ruling ally-echo-bistatic-oval). This commit builds and replaces the sensor the previous commit's tests were written against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e1827c9db0
commit
dd896528cc
4 changed files with 71 additions and 36 deletions
|
|
@ -66,10 +66,9 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
private float _pingLerp;
|
||||
private bool _pinging;
|
||||
private float _pingRadius;
|
||||
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>();
|
||||
// The subjects this ping's wavefront has reached, in the beam or not; each is crossed at most once per ping.
|
||||
private readonly HashSet<Entity> _reached = new HashSet<Entity>();
|
||||
|
||||
public float Cooldown
|
||||
{
|
||||
|
|
@ -81,22 +80,15 @@ 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)
|
||||
// Whether the offset `diff` (length `dist`) from this sensor lies inside its beam.
|
||||
private bool InBeam(float2 diff, float dist)
|
||||
{
|
||||
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);
|
||||
var beam = BeamWidth;
|
||||
return beam >= 360f || dot(Facing, diff) >= dist * cos(beam * .5f * .0174533f);
|
||||
}
|
||||
|
||||
public float PingBrightness => pow(1 - _pingLerp, _data.PingRadiusExponent);
|
||||
|
|
@ -145,6 +137,7 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
_pingCooldown = 1;
|
||||
_pingLerp = 0;
|
||||
_pingRadius = 0;
|
||||
_reached.Clear();
|
||||
OnPingStart?.Invoke();
|
||||
}
|
||||
}
|
||||
|
|
@ -165,7 +158,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;
|
||||
var sweeping = _pinging;
|
||||
if (_pinging)
|
||||
{
|
||||
_pingLerp += dt / _data.PingDuration;
|
||||
|
|
@ -180,13 +173,6 @@ 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;
|
||||
|
|
@ -197,31 +183,46 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
float previous, next;
|
||||
Entity.EntityInfoGathered.TryGetValue(entity, out previous);
|
||||
|
||||
// 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))
|
||||
{
|
||||
crossings++;
|
||||
entity.ReceivePaint(Entity.Position, Item?.Data, PaintMode.Search, 0);
|
||||
}
|
||||
foreach (var pinger in _alliedPingers)
|
||||
if (pinger.Entity != entity && Crossing(pinger, entity)) crossings++;
|
||||
var sensitivity = Evaluate(_data.Sensitivity);
|
||||
var response = _data.PingBoost == null ? 0f : Evaluate(_data.PingBoost);
|
||||
var credit = 0f;
|
||||
var heard = false;
|
||||
|
||||
if (crossings > 0)
|
||||
// The first sweeping tick this ping's wavefront is past the entity is its one crossing. Inside the beam
|
||||
// it paints the subject (it learns it is swept), credits this sensor, and is returned to every ally; an
|
||||
// ally hears it silently (ruling sensor-stat-set item 3): credit, no paint, no emission.
|
||||
if (sweeping && dist < _pingRadius && _reached.Add(entity) && InBeam(diff, dist))
|
||||
{
|
||||
var response = _data.PingBoost == null ? 0f : Evaluate(_data.PingBoost);
|
||||
next = saturate(previous + crossings * Gain(entity.Visibility, Evaluate(_data.Sensitivity), response, dist, dt, true, obscuration));
|
||||
heard = true;
|
||||
entity.ReceivePaint(Entity.Position, Item?.Data, PaintMode.Search, 0);
|
||||
credit += Gain(entity.Visibility, sensitivity, response, dist, dt, true, obscuration);
|
||||
var pingReturn = new PingReturn(entity, dist, obscuration, Evaluate(_data.PingRange));
|
||||
foreach (var ally in Entity.Zone.Entities)
|
||||
if (ally != Entity && ally != entity && ally.Faction != null && ally.Faction == Entity.Faction && !ally.IsHostileTo(Entity))
|
||||
ally.ReceiveReturn(pingReturn);
|
||||
}
|
||||
|
||||
// An ally's ping returned off this subject (ruling ally-echo-bistatic-oval): heard while the
|
||||
// illuminated and received ranges sit inside the oval Rt x Rr < PingRange^2, credited at their geometric mean.
|
||||
foreach (var r in Entity.PingReturns)
|
||||
{
|
||||
if (r.Subject != entity || r.IlluminatedRange * dist >= r.MaxRange * r.MaxRange) continue;
|
||||
heard = true;
|
||||
credit += Gain(entity.Visibility, sensitivity, response, sqrt(r.IlluminatedRange * dist), dt, true, sqrt(r.IlluminatedObscuration * obscuration));
|
||||
}
|
||||
|
||||
if (heard)
|
||||
next = saturate(previous + credit);
|
||||
else
|
||||
{
|
||||
var angle = acos(dot(forward, normalize(diff)));
|
||||
next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), dist, dt, false, obscuration));
|
||||
next = saturate(previous + Gain(entity.Visibility, sensitivity, _data.SensitivityCurve.Evaluate(angle / PI), dist, dt, false, obscuration));
|
||||
}
|
||||
next *= 1 - ItemManager.GameplaySettings.TargetInfoDecay * dt;
|
||||
//Context.Log($"{entity.Name} visibility {(int)(previous * 100)}% -> {(int)(next * 100)}%");
|
||||
Entity.EntityInfoGathered[entity] = next;
|
||||
}
|
||||
if (!_pinging) _reached.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -187,11 +187,16 @@ public abstract class Entity
|
|||
public ReactiveProperty<int> TargetedByCount = new ReactiveProperty<int>(0);
|
||||
// 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
|
||||
// inbox the tick it paints; PublishInbox 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;
|
||||
// An ally's sensor posts a return on the tick its ping crosses a subject; published with paints, read by this
|
||||
// ship's own sensors for one tick.
|
||||
private readonly List<PingReturn> _returnInbox = new List<PingReturn>();
|
||||
private readonly List<PingReturn> _pingReturns = new List<PingReturn>();
|
||||
internal IReadOnlyList<PingReturn> PingReturns => _pingReturns;
|
||||
public ReactiveProperty<SecurityLevel> CurrentSecurityLevel = new ReactiveProperty<SecurityLevel>(SecurityLevel.Open);
|
||||
public ReadOnlyReactiveProperty<bool> PresencePermitted;
|
||||
|
||||
|
|
@ -804,16 +809,27 @@ public abstract class Entity
|
|||
VisibleFriendlies.Clear();
|
||||
_paintInbox.Clear();
|
||||
_incomingPaints.Clear();
|
||||
_returnInbox.Clear();
|
||||
_pingReturns.Clear();
|
||||
}
|
||||
|
||||
// Zone.Update runs this on every entity before any entity updates, so a paint made during tick N is published
|
||||
// at the start of tick N+1 whatever order the emitter and this ship update in; a paint that stops is gone
|
||||
// one tick later for every ordering.
|
||||
internal void PublishPaints()
|
||||
internal void PublishInbox()
|
||||
{
|
||||
_incomingPaints.Clear();
|
||||
_incomingPaints.AddRange(_paintInbox);
|
||||
_paintInbox.Clear();
|
||||
_pingReturns.Clear();
|
||||
_pingReturns.AddRange(_returnInbox);
|
||||
_returnInbox.Clear();
|
||||
}
|
||||
|
||||
internal void ReceiveReturn(PingReturn pingReturn)
|
||||
{
|
||||
if (!_active) return;
|
||||
_returnInbox.Add(pingReturn);
|
||||
}
|
||||
|
||||
// The callers are Sensor.Execute (a Search paint, Strength 0) and LockWeapon.Execute (a Track paint, Strength
|
||||
|
|
|
|||
|
|
@ -40,6 +40,24 @@ public enum PaintMode
|
|||
Track
|
||||
}
|
||||
|
||||
// An ally's ping crossed `Subject` at `IlluminatedRange` through `IlluminatedObscuration`; `MaxRange` is that
|
||||
// sensor's PingRange. Posted on the crossing tick and read for one tick by the receiving ship's own sensors.
|
||||
public readonly struct PingReturn
|
||||
{
|
||||
public readonly Entity Subject;
|
||||
public readonly float IlluminatedRange;
|
||||
public readonly float IlluminatedObscuration;
|
||||
public readonly float MaxRange;
|
||||
|
||||
public PingReturn(Entity subject, float illuminatedRange, float illuminatedObscuration, float maxRange)
|
||||
{
|
||||
Subject = subject;
|
||||
IlluminatedRange = illuminatedRange;
|
||||
IlluminatedObscuration = illuminatedObscuration;
|
||||
MaxRange = maxRange;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct PaintWarning
|
||||
{
|
||||
public readonly float2 Bearing;
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ public class Zone
|
|||
agent.Update(deltaTime);
|
||||
|
||||
var entities = Entities.ToArray();
|
||||
foreach (var entity in entities) entity.PublishPaints();
|
||||
foreach (var entity in entities) entity.PublishInbox();
|
||||
foreach (var entity in entities) entity.Update(deltaTime);
|
||||
|
||||
StepMines(deltaTime);
|
||||
|
|
|
|||
Loading…
Reference in a new issue