Scanner decays from the zone clock, not tick order; revert the consumable reorder
bdc8f1d0ran Execute before the always-updated pass in ConsumableItemEffect.Update; that moved StatModifier timing for every consumable (TheOverdriveLocksAndBoosts failed). The scan's decay is now derived from the zone clock since the last held tick (Scanner.Progress, one site), so it is the same on the tick it breaks for an equipment host and a consumable host, and Scanner is no longer an IAlwaysUpdatedBehavior. The pilot-path test attaches a task-less Agent: a patrolling Minion needs zone orbits.bdc8f1d0does not pass its own suite; this commit is the intended state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bdc8f1d048
commit
b26fbbaa95
4 changed files with 34 additions and 29 deletions
|
|
@ -36,13 +36,18 @@ public class ScannerData : BehaviorData
|
|||
}
|
||||
}
|
||||
|
||||
public class Scanner : Behavior, IAlwaysUpdatedBehavior, IProgressBehavior
|
||||
public class Scanner : Behavior, IProgressBehavior
|
||||
{
|
||||
private readonly ScannerData _data;
|
||||
private Entity _target;
|
||||
private bool _held;
|
||||
private float _built;
|
||||
private double _heldAt;
|
||||
|
||||
public float Progress { get; private set; }
|
||||
// The scan's progress now: what Execute last built, less ScanTime-relative decay over the time since a tick last held
|
||||
// it (ruling scan-held-beam: a broken scan decays over ScanTime, the item inactive or destroyed included). Derived from
|
||||
// the zone clock, never from tick order, so a scanner decays on the tick it breaks whether its host runs Execute
|
||||
// before the always-updated pass (EquippedItem) or after it (ConsumableItemEffect). The one decay site.
|
||||
public float Progress => Remaining(_heldAt);
|
||||
|
||||
public Scanner(ScannerData data, EquippedItem item) : base(data, item)
|
||||
{
|
||||
|
|
@ -53,40 +58,41 @@ public class Scanner : Behavior, IAlwaysUpdatedBehavior, IProgressBehavior
|
|||
_data = data;
|
||||
}
|
||||
|
||||
private float Remaining(double since)
|
||||
{
|
||||
if (_built <= 0f) return 0f;
|
||||
var now = Entity.Zone?.ExactTime ?? since;
|
||||
return saturate(_built - (float) (now - since) / Evaluate(_data.ScanTime));
|
||||
}
|
||||
|
||||
// Runs only while the item is Active. The scan builds while the ship's own Target is a tracked ship within Range, and
|
||||
// is painted and emitted on every tick it builds; at 1 the target's fitted items enter the book and the scan starts over.
|
||||
// A tick that does not hold the scan builds nothing, and the zone clock carries its decay (Progress).
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
var target = Entity.Target.Value.Entity;
|
||||
if (_target != target)
|
||||
{
|
||||
Progress = 0;
|
||||
_built = 0f;
|
||||
_target = target;
|
||||
}
|
||||
|
||||
var range = Evaluate(_data.Range);
|
||||
if (target != null && Entity.VisibleEntities.Contains(target) && Entity.TargetRange <= range)
|
||||
{
|
||||
Progress = saturate(Progress + dt / Evaluate(_data.ScanTime));
|
||||
target.ReceivePaint(Entity, Item, PaintMode.Scan, Progress);
|
||||
// Decay over the ticks since the last held one, not counting this tick, which holds.
|
||||
var progress = saturate(Remaining(_heldAt + dt) + dt / Evaluate(_data.ScanTime));
|
||||
_built = progress;
|
||||
_heldAt = Entity.Zone?.ExactTime ?? _heldAt;
|
||||
target.ReceivePaint(Entity, Item, PaintMode.Scan, progress);
|
||||
Entity.Emit(Item, range / (_data.InterceptFactor == null ? 1f : Evaluate(_data.InterceptFactor)));
|
||||
_held = true;
|
||||
if (Progress >= 1f)
|
||||
if (progress >= 1f)
|
||||
{
|
||||
Entity.RecordInspection(target);
|
||||
Progress = 0;
|
||||
_built = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Every tick, after Execute on either host (EquippedItem.Update and ConsumableItemEffect.Update both run Execute first):
|
||||
// a scan not held this tick (out of range, untracked, or the item inactive so Execute did
|
||||
// not run) decays over ScanTime (ruling scan-held-beam). The one decay site.
|
||||
public void Update(float delta)
|
||||
{
|
||||
if (!_held) Progress = saturate(Progress - delta / Evaluate(_data.ScanTime));
|
||||
_held = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1955,19 +1955,17 @@ public class ConsumableItemEffect : IStatContext
|
|||
.ToArray();
|
||||
}
|
||||
|
||||
// Execute, then the always-updated pass, as EquippedItem.Update orders them: an always-updated behaviour that reads
|
||||
// what Execute marked this tick (Scanner's held mark) sees the same tick on either host.
|
||||
public void Update(float delta)
|
||||
{
|
||||
foreach (var behavior in Behaviors)
|
||||
if(behavior is IAlwaysUpdatedBehavior alwaysUpdatedBehavior) alwaysUpdatedBehavior.Update(delta);
|
||||
|
||||
foreach (var behavior in Behaviors)
|
||||
{
|
||||
if (!behavior.Execute(delta))
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var behavior in Behaviors)
|
||||
if(behavior is IAlwaysUpdatedBehavior alwaysUpdatedBehavior) alwaysUpdatedBehavior.Update(delta);
|
||||
|
||||
RemainingDuration -= delta;
|
||||
// Progress moves every tick; a stat with a ConsumableProgress term must recompute against it, the same
|
||||
// way EquippedItem signals Heat and Durability from UpdatePerformance below.
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
|
||||
// A ship at `position` with `mounts` on its sensor hardpoints in order, the tools on free cells and a thruster
|
||||
// on the centre column. A ship with a gun also gets the reactor and targeting a gun needs to fire.
|
||||
private Ship Add(float3 position, string[] mounts = null, string[] tools = null, bool thruster = false, bool agent = false)
|
||||
private Ship Add(float3 position, string[] mounts = null, string[] tools = null, bool thruster = false)
|
||||
{
|
||||
var hullRef = _items.ItemData.RefOf<ItemData>(_items.ItemData.GetByName<HullData>("Rack"));
|
||||
var ship = new Ship(_items, _zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = _nextLot++ }, new EntitySettings())
|
||||
|
|
@ -269,7 +269,7 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
Assert.True(ship.TryFindSpace(item, out var at));
|
||||
Assert.True(ship.TryEquip(item, at));
|
||||
}
|
||||
_zone.Admit(ship, piloted: agent); // piloted: true gives the ship an AI agent
|
||||
_zone.Admit(ship, piloted: false);
|
||||
ship.Position = position;
|
||||
ship.Aim = float3(0, 0, 1);
|
||||
_zone.Update(0f); // brings the gear online
|
||||
|
|
|
|||
|
|
@ -321,12 +321,12 @@ public sealed partial class ContactRevealTests
|
|||
[Fact]
|
||||
public void AScanTakesTheSamePathForAnyPilot()
|
||||
{
|
||||
_zone.AddOrbit(new OrbitData { Distance = 1000f }); // the agent patrols one; an orbit-less zone gives its circuit no first stop
|
||||
var target = Add(float3(0, 0, 100), tools: new[] { "Reactor", "Radiator", "SuppressorCold" });
|
||||
var reactor = target.Equipment.Single(i => i.Behaviors.Any(b => b is Reactor));
|
||||
target.Claim(reactor, DataOf("Radiator"));
|
||||
var player = Add(float3(-20, 0, 0), tools: new[] { "Scanner" });
|
||||
var ai = Add(float3(20, 0, 0), tools: new[] { "Scanner" }, agent: true);
|
||||
var ai = Add(float3(20, 0, 0), tools: new[] { "Scanner" });
|
||||
_zone.Agents.Add(new Agent(ai)); // the AI pilot, ticked in Zone.Update beside the scan; a task-less agent needs no orbit to patrol
|
||||
Assert.Single(_zone.Agents); // fixture: only the second ship has an agent
|
||||
foreach (var scanner in new[] { player, ai })
|
||||
{
|
||||
|
|
@ -356,8 +356,9 @@ public sealed partial class ContactRevealTests
|
|||
|
||||
// ---- AConsumableScannerDecaysOnTheSameTick ----
|
||||
|
||||
// Equipment and consumable hosts run Execute before the always-updated pass, so a scan broken by range loses the same
|
||||
// dt/ScanTime on the tick it breaks: .5 falls to .25 in four ticks, as ABrokenScanDecays has it for gear.
|
||||
// The two hosts order Execute and the always-updated pass differently (ConsumableItemEffect.Update runs the pass first),
|
||||
// so the scan's decay must not depend on that order: a scan broken by range loses dt/ScanTime on the tick it breaks, and
|
||||
// .5 falls to .25 in four ticks, as ABrokenScanDecays has it for gear.
|
||||
[Fact]
|
||||
public void AConsumableScannerDecaysOnTheSameTick()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue