Sensor.Gain takes the obscuration of its sight line
Gain stays the one detection rule: both branches are scaled by the fraction of the signal that survives the clouds between observer and target (Zone.Obscuration, exactly 1f with none, so the pre-cloud trace is bit-identical). The entity loop passes it for pings and passive ticks and PassiveRate for the chunk position. Entity.cs is untouched: a lock falls by the existing detection-threshold rule when info does. Adds the sensor-level facts to VapourCloudTests. Builds; not yet run (verify job follows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6dcf33d11c
commit
0fb5ac1dfc
2 changed files with 91 additions and 8 deletions
|
|
@ -160,17 +160,18 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
|
||||
var diff = entity.Position.xz - Entity.Position.xz;
|
||||
var dist = length(diff);
|
||||
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)
|
||||
{
|
||||
_pingedEntities.Add(entity);
|
||||
next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), Evaluate(_data.PingBoost), dist, dt, true));
|
||||
next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.Sensitivity), Evaluate(_data.PingBoost), dist, dt, true, obscuration));
|
||||
}
|
||||
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));
|
||||
next = saturate(previous + Gain(entity.Visibility, Evaluate(_data.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)}%");
|
||||
|
|
@ -182,12 +183,14 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
// Mining Cut 3 (docs/mining-cut-refresh.md): the one detection gain rule -- the info one observation of a
|
||||
// target adds. A ping adds `visibility × sensitivity × PingBoost × distance` once; a passive tick adds
|
||||
// `visibility × sensitivity × curve(angle/π) × dt / distance`. `response` is PingBoost for a ping and the
|
||||
// sensitivity curve at the bearing for a passive tick. The entity loop above and the chunk query
|
||||
// (Entity.ChunkInfo, through PassiveRate) both call this; nothing else multiplies these terms.
|
||||
public static float Gain(float visibility, float sensitivity, float response, float distance, float dt, bool ping) =>
|
||||
ping
|
||||
// sensitivity curve at the bearing for a passive tick. `obscuration` is the fraction of the signal that
|
||||
// survives the vapour clouds on the sight line (Zone.Obscuration, 1f with none), scaling both. The entity
|
||||
// loop above and the chunk query (Entity.ChunkInfo, through PassiveRate) both call this; nothing else
|
||||
// multiplies these terms.
|
||||
public static float Gain(float visibility, float sensitivity, float response, float distance, float dt, bool ping, float obscuration) =>
|
||||
(ping
|
||||
? visibility * sensitivity * response * distance
|
||||
: visibility * sensitivity * response * dt / distance;
|
||||
: visibility * sensitivity * response * dt / distance) * obscuration;
|
||||
|
||||
// The passive gain per second this sensor draws from a target of `visibility` at the planar `position`:
|
||||
// Gain over one second, with the bearing measured from this sensor's own facing exactly as Execute does.
|
||||
|
|
@ -195,7 +198,8 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
{
|
||||
var diff = position - Entity.Position.xz;
|
||||
var angle = acos(dot(Direction.xz, normalize(diff)));
|
||||
return Gain(visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), length(diff), 1f, false);
|
||||
return Gain(visibility, Evaluate(_data.Sensitivity), _data.SensitivityCurve.Evaluate(angle / PI), length(diff), 1f, false,
|
||||
Entity.Zone.Obscuration(Entity.Position.xz, position));
|
||||
}
|
||||
|
||||
// An upper bound on sensitivity x response over every bearing, so PassiveRate(v, p) <= v x this / distance
|
||||
|
|
|
|||
|
|
@ -189,6 +189,56 @@ public sealed class VapourCloudTests : IDisposable
|
|||
return cloud;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ACloudBreaksALockAtRange()
|
||||
{
|
||||
var lab = Build(400f);
|
||||
Establish(lab);
|
||||
|
||||
Vent(lab);
|
||||
var ticks = 0;
|
||||
while (ticks++ < 600 && !lab.Observer.Target.Value.IsNone) Tick(lab);
|
||||
|
||||
Assert.True(lab.Observer.Target.Value.IsNone, "the cloud never broke the target");
|
||||
Assert.True(Info(lab.Observer, lab.Target) < Threshold, $"info {Info(lab.Observer, lab.Target)} is above the threshold");
|
||||
Run(lab, 3);
|
||||
Assert.Equal(0f, lab.Lock.Lock);
|
||||
Assert.DoesNotContain(lab.Target, lab.Observer.VisibleEnemies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AStrongCloseSensorBurnsThrough()
|
||||
{
|
||||
var lab = Build(40f);
|
||||
Establish(lab);
|
||||
var before = Info(lab.Observer, lab.Target);
|
||||
|
||||
Vent(lab);
|
||||
Run(lab, 600);
|
||||
|
||||
var after = Info(lab.Observer, lab.Target);
|
||||
Assert.True(after < before - .2f, $"the cloud thinned nothing: {before} -> {after}");
|
||||
Assert.True(after > Threshold, $"info {after} fell below the threshold");
|
||||
Assert.Equal(lab.Target, lab.Observer.Target.Value.Entity);
|
||||
Assert.True(lab.Lock.IsLocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheCloudBlindsItsVenter()
|
||||
{
|
||||
var lab = Build(400f);
|
||||
Run(lab, 100);
|
||||
Assert.True(Info(lab.Target, lab.Observer) > Threshold, "the venter never gathered the observer");
|
||||
Assert.Contains(lab.Observer, lab.Target.VisibleEnemies);
|
||||
|
||||
Vent(lab);
|
||||
Run(lab, 300);
|
||||
|
||||
Assert.True(Info(lab.Target, lab.Observer) < Threshold);
|
||||
Assert.DoesNotContain(lab.Observer, lab.Target.VisibleEnemies);
|
||||
Assert.Equal(Info(lab.Observer, lab.Target), Info(lab.Target, lab.Observer), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ACloudFadesAndIsRemoved()
|
||||
{
|
||||
|
|
@ -270,4 +320,33 @@ public sealed class VapourCloudTests : IDisposable
|
|||
Assert.Equal(.2f, Through(float2(-50, 0), float2(50, 0)), 5);
|
||||
Assert.Equal(.4f, Through(float2(-50, 0), float2(15, 0)), 5);
|
||||
}
|
||||
|
||||
// Where the rule is decided: one tick from nothing, the info gathered is exactly what survives the cloud
|
||||
// times what the open sight line gathers, in both directions.
|
||||
[Fact]
|
||||
public void TheGainIsScaledByWhatSurvivesTheCloud()
|
||||
{
|
||||
var clear = Build(100f);
|
||||
Tick(clear);
|
||||
var cloudy = Build(100f);
|
||||
Hand(cloudy.Zone, float2(0, 100), 20f, .6f);
|
||||
Tick(cloudy);
|
||||
|
||||
Assert.True(Info(clear.Observer, clear.Target) > 0f);
|
||||
Assert.Equal(.4f, Info(cloudy.Observer, cloudy.Target) / Info(clear.Observer, clear.Target), 3);
|
||||
Assert.Equal(.4f, Info(cloudy.Target, cloudy.Observer) / Info(clear.Target, clear.Observer), 3);
|
||||
}
|
||||
|
||||
// The chunk path reads the same rule: the passive rate toward a point is scaled by the same survivor fraction.
|
||||
[Fact]
|
||||
public void TheChunkRateIsScaledByTheSameFactor()
|
||||
{
|
||||
var lab = Build(100f);
|
||||
var at = float2(0, 100);
|
||||
var open = lab.Observer.Sensor.PassiveRate(100f, at);
|
||||
Assert.True(open > 0f);
|
||||
|
||||
Hand(lab.Zone, at, 20f, .6f);
|
||||
Assert.Equal(.4f, lab.Observer.Sensor.PassiveRate(100f, at) / open, 5);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue