diff --git a/Assets/Scripts/ServerShared/RunStart.cs b/Assets/Scripts/ServerShared/RunStart.cs index 2e1245bab..20269e84c 100644 --- a/Assets/Scripts/ServerShared/RunStart.cs +++ b/Assets/Scripts/ServerShared/RunStart.cs @@ -55,6 +55,12 @@ public static class RunStart player.SetIff(placed.Entity, hostile); arena.Admit(placed.Entity, placed.Piloted, placed.Task); } + + // A start state includes what a pilot knows (ruling stage-states-pilot-knowledge): its anchor is tracked from the first + // step, by the sensors' own path (info gathered past the detection threshold), so they can lose it like any track. + foreach (var placed in stage.Placed) + if (placed.Knows != null && placed.Piloted) + placed.Entity.EntityInfoGathered[placed.Knows] = (itemManager.GameplaySettings.TargetDetectionInfoThreshold + 1f) / 2f; return new Staged { Player = player, Credits = stage.Credits, Entities = stage.Placed.Select(placed => placed.Entity).ToList() }; } diff --git a/Assets/Scripts/ServerShared/ScenarioStage.cs b/Assets/Scripts/ServerShared/ScenarioStage.cs index f0df352e9..a9e2db5d0 100644 --- a/Assets/Scripts/ServerShared/ScenarioStage.cs +++ b/Assets/Scripts/ServerShared/ScenarioStage.cs @@ -50,6 +50,9 @@ public sealed class ScenarioStage public ScenarioStance Stance; public bool Piloted; public AgentTask Task; + // What the pilot already knows at the start: an entity its sensors hold a track of (Approach). Seeded through the + // sensors' own state at admission, so they may lose it honestly. + public Entity Knows; } // A hull fitted slot by slot: each slot a design or a product, the hull cell it goes in, and its rotation. A name is @@ -137,7 +140,7 @@ public sealed class ScenarioStage } // The task a piloted entity starts on: it closes on the target and holds there (the flight's follow task, standoff - // nothing), in place of the patrol circuit the zone would draw for it. A check that rests on where a pilot goes states + // nothing), in place of the patrol circuit the zone would draw for it. The pilot starts knowing it (RunStart seeds a sensor track). A check that rests on where a pilot goes states // it here, so the pilot's course is the scenario's and not a draw. An entity that is not placed and piloted, or a // target that is not placed, is a failure. public void Approach(Entity entity, Entity target) @@ -146,6 +149,7 @@ public sealed class ScenarioStage if (placement == null || !placement.Piloted) { _failures.Add($"approach: {entity?.Name ?? "no entity"} is not a placed, piloted ship"); return; } if (target == null) { _failures.Add("approach: no target to close on"); return; } placement.Task = new FollowTask { Anchor = PilotView.Handle(target), Standoff = 0f }; + placement.Knows = target; } // Items in the entity's first cargo bay, one per name. A name is an equippable or consumable design (open: the stage diff --git a/tests/Aetheria.Shared.Tests/ApproachKnowledgeTests.cs b/tests/Aetheria.Shared.Tests/ApproachKnowledgeTests.cs new file mode 100644 index 000000000..98f857913 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/ApproachKnowledgeTests.cs @@ -0,0 +1,60 @@ +using System.Linq; +using CultMath; +using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; + +// A staged Approach states what its pilot already knows (ruling stage-states-pilot-knowledge): the anchor is a sensor +// track from the first step, and the pilot follows it only as long as the sensors keep it. +public sealed partial class RunStartTests +{ + private (Zone arena, Ship east, Ship west) StagedApproach() + { + Ship east = null, west = null; + var (_, arena, _, failures) = Launch(new Scripted(false, stage => + { + stage.Player(stage.Bare("Djinni"), float2(-50000, -50000)); + east = stage.Place(EvLonginus(stage), float2(450, 0), facing: float2(-1, 0), piloted: true) as Ship; + west = stage.Place(EvLonginus(stage), float2(-450, 0), facing: float2(1, 0), piloted: true) as Ship; + stage.Approach(east, west); + })); + Assert.True(failures.Count == 0, string.Join("; ", failures)); + return (arena, east, west); + } + + [Fact] + public void AStagedApproachFollowerClosesOnItsAnchorFromTheFirstStep() + { + var (arena, east, west) = StagedApproach(); + var view = arena.Agents.Single(a => a.Ship == east).View; + var startGap = east.Position.x - west.Position.x; + + arena.Update(1f / 60f); + arena.Update(1f / 60f); + + Assert.NotNull(view.TrackOf(east, PilotView.Handle(west))); + Assert.True(east.Velocity.x < -.5f, $"the follower thrusts toward its anchor from the first steps (vx {east.Velocity.x})"); + for (var i = 0; i < 120; i++) arena.Update(1f / 60f); + Assert.True(east.Position.x - west.Position.x < startGap - 20f, "the follower closed on its anchor"); + } + + [Fact] + public void AStagedApproachFollowerStopsFollowingOnceTheSensorsLoseTheTrack() + { + var (arena, east, west) = StagedApproach(); + var view = arena.Agents.Single(a => a.Ship == east).View; + var sentinel = new float3(0, 0, 12345); + for (var i = 0; i < 3; i++) arena.Update(1f / 60f); + + east.Command.Aim = sentinel; + arena.Update(1f / 60f); + Assert.NotEqual(sentinel, east.Command.Aim.Value); + + east.EntityInfoGathered[west] = 0f; + Assert.Null(view.TrackOf(east, PilotView.Handle(west))); + east.Command.Aim = sentinel; + arena.Update(1f / 60f); + Assert.Equal(sentinel, east.Command.Aim.Value); + } +}