diff --git a/Assets/Scripts/ServerShared/Scenarios/LockOn.cs b/Assets/Scripts/ServerShared/Scenarios/LockOn.cs index 0aab7dabe..4734ab8ef 100644 --- a/Assets/Scripts/ServerShared/Scenarios/LockOn.cs +++ b/Assets/Scripts/ServerShared/Scenarios/LockOn.cs @@ -10,10 +10,11 @@ public sealed class LockOn : Scenario public override uint Seed => 113; public override bool Ambient => false; - // The hostile starts far beyond the player's sight and the hostile's own, 4000 m ahead facing the player. Stepping the - // arena headlessly, it closes to about 3000 m, sees the player after some eight seconds, and locks two ticks later; - // at Duel's 900 m (and at 600 m) it sees and locks within a second. - public static readonly CultMath.float2 HostileAt = float2(0, 4000); + // The hostile starts about 4000 m off, to the side of the player's axis, facing the player. At that range its passive + // sensors hold no track of the player (the test asks the sim's own PassiveInfo against the detection threshold); the + // player sees the hostile a little sooner, at about tick 20. The hostile closes, sees the player some eight seconds in, + // and locks two ticks later. At Duel's 900 m (and at 600 m) it sees and locks within a second. + public static readonly CultMath.float2 HostileAt = float2(1500, 3700); public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(DemoTerminus.Cast); @@ -38,6 +39,6 @@ public sealed class LockOn : Scenario ("Store-All Plus", int2(2, 8), None)); stage.Player(fighter, float2(0, 0), facing: float2(0, 1)); - stage.Place(fighter, HostileAt, facing: float2(0, -1), stance: ScenarioStance.Hostile, piloted: true); + stage.Place(fighter, HostileAt, facing: normalize(-HostileAt), stance: ScenarioStance.Hostile, piloted: true); } } diff --git a/tests/Aetheria.Shared.Tests/LockOnScenarioTests.cs b/tests/Aetheria.Shared.Tests/LockOnScenarioTests.cs index 3af2fef45..36c0136da 100644 --- a/tests/Aetheria.Shared.Tests/LockOnScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/LockOnScenarioTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Xunit; using static CultMath.math; @@ -6,9 +7,9 @@ using static CultMath.math; // test launches the scenario the way New Game does and steps what it staged. public sealed partial class RunStartTests { - // Lock On: the hostile starts piloted, beyond its sight of the player, and the player has no incoming paint. Stepped at - // .1 s, it closes, finds the player and locks: a Track paint reaches the player bearing toward the hostile, and not at - // once, because the hostile had to find the player first. The tick is the lock's, at seed 113 it is 83 (8.4 s). + // Lock On: the hostile starts piloted, facing the player, beyond its own sight of the player, and the player has no + // incoming paint. Stepped at .1 s, it closes, finds the player and locks: a Track paint reaches the player bearing + // toward the hostile, and its Strength rises from nothing as the lock tightens. [Fact] public void LockOnSituation() { @@ -19,25 +20,38 @@ public sealed partial class RunStartTests Assert.True(player.IsHostileTo(hostile) && hostile.IsHostileTo(player), "the hostile is hostile both ways"); Assert.Equal(1, AgentsOf(arena, hostile)); - arena.Update(0f); - Assert.DoesNotContain(hostile, player.VisibleEnemies); + var toPlayer = normalize(player.Position.xz - hostile.Position.xz); + Assert.True(dot(normalize(hostile.Direction), toPlayer) >= cos(radians(5f)), "the hostile faces the player"); + + // The first real sensing tick: the hostile holds no track of the player, and the sim's own passive sensing, asked for + // the player's signature at the staged place, settles below the detection threshold: the staging is out of its sight. + arena.Update(.1f); + Assert.DoesNotContain(player, hostile.VisibleEnemies); Assert.DoesNotContain(player.IncomingPaints, paint => paint.Mode == PaintMode.Track); - Assert.DoesNotContain(player, hostile.VisibleEnemies); // it starts without a track of the player + Assert.True(player.Visibility > 0f, "the player shows a signature to be seen by"); + var threshold = player.ItemManager.GameplaySettings.TargetDetectionInfoThreshold; + var settled = hostile.PassiveInfo(player.Position.xz, player.Visibility); + Assert.True(settled <= threshold, $"the hostile would settle at {settled} on the player, above the detection threshold {threshold}: staged inside its sight"); var lockedAt = -1; var bearingDot = 0f; - for (var tick = 0; tick < 600 && lockedAt < 0; tick++) + var strengths = new List(); + for (var tick = 0; tick < 600 && (lockedAt < 0 || tick < lockedAt + 40); tick++) { arena.Update(.1f); foreach (var paint in player.IncomingPaints.Where(paint => paint.Mode == PaintMode.Track)) { - lockedAt = tick; - bearingDot = dot(normalize(paint.Bearing), normalize(hostile.Position.xz - player.Position.xz)); + if (lockedAt < 0) + { + lockedAt = tick; + bearingDot = dot(normalize(paint.Bearing), toPlayer * -1f); + } + strengths.Add(paint.Strength); } } Assert.True(lockedAt >= 0, "the hostile never locked the player within 600 ticks of .1 s"); Assert.True(bearingDot >= cos(radians(5f)), "the warning points at the hostile"); - // Not at the staging: the hostile had to close and find the player first (a hostile staged inside sight locks within 10 ticks). - Assert.InRange(lockedAt, 40, 200); + Assert.True(strengths.First() < strengths.Last(), $"the lock tightens: Strength went {strengths.First()} to {strengths.Last()}"); + Assert.True(strengths.Zip(strengths.Skip(1), (a, b) => b >= a).All(rising => rising), "the lock never loosens while it holds"); } }