Staged Approach: the pilot starts knowing its anchor, through the sensors' own info state (ruling stage-states-pilot-knowledge)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
soul 2026-10-22 16:00:08 +02:00
commit d5d94fb483
3 changed files with 71 additions and 1 deletions

View file

@ -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() };
}

View file

@ -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

View file

@ -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);
}
}