Crossing: the crosser starts to port at 50 m/s and the test observes the pass over simulated time; fit and hull pinned

Not yet verified at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 19:57:16 +02:00
commit 4f12d581cb
2 changed files with 38 additions and 6 deletions

View file

@ -5,15 +5,17 @@ public sealed class Crossing : Scenario
{
public override string Name => "Crossing";
public override string Brief => "plight at the starboard gun and a FastBlast+- at the port gun, a fast Djinni crossing close ahead and an " +
"unpiloted one waiting at range. Verify: as the Djinni crosses close, FastBlast lands some and plight " +
"unpiloted one waiting at range. The Djinni starts to port moving across the bow; the arena's gravity bends its run. Verify: as the Djinni crosses close, FastBlast lands some and plight " +
"almost none; against the Djinni at range both land. Fly it again without the Fire Control Array: " +
"with it the small gun lands more.";
public override uint Seed => 112;
public override bool Ambient => false;
// Where the Djinni sit: the player is at the origin facing +z. The crosser starts ahead and to starboard, facing
// across the bow, and flies in under 400 m. The waiting Djinni sits inside plight's arc at 80% of its Range.
public static readonly CultMath.float2 CrosserAt = float2(250, 280), CrosserFacing = float2(-1, 0);
// Where the Djinni sit: the player is at the origin facing +z. The crosser starts ahead and to port, facing across
// the bow and already moving at the Djinni's 50 m/s cap, so its run sweeps across the bow within seconds (the arena
// pulls the player to port, which brings the two together). The waiting Djinni sits inside plight's arc at 80% of
// its Range.
public static readonly CultMath.float2 CrosserAt = float2(-150, 200), CrosserFacing = float2(1, 0), CrosserVelocity = float2(50, 0);
public const float PlightRange = 1216f; // plight's Range in metres, 1216.4 as the fit stands settled at rest
public static readonly CultMath.float2 Waiting = float2(0, PlightRange * .8f);
@ -38,7 +40,7 @@ public sealed class Crossing : Scenario
("Fire Control Array", int2(1, 6), Clockwise));
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));
stage.Place(stage.Generated("Djinni"), CrosserAt, facing: CrosserFacing, stance: ScenarioStance.Hostile, piloted: true);
stage.Place(stage.Generated("Djinni"), CrosserAt, facing: CrosserFacing, stance: ScenarioStance.Hostile, piloted: true, velocity: CrosserVelocity);
stage.Place(stage.Bare("Djinni"), Waiting);
}
}

View file

@ -32,6 +32,8 @@ public sealed partial class RunStartTests
var waiting = staged.Entities[1];
Assert.IsType<Ship>(crosser);
Assert.IsType<Ship>(waiting);
Assert.Equal(("Djinni", "Djinni"), (_items.GetData(crosser.Hull).Name, _items.GetData(waiting.Hull).Name));
Assert.Contains(crosser.Equipment, item => item.Behaviors.OfType<Weapon>().Any());
Assert.True(player.IsHostileTo(crosser) && crosser.IsHostileTo(player), "the crosser is hostile both ways");
Assert.Equal(1, AgentsOf(arena, crosser));
@ -49,8 +51,30 @@ public sealed partial class RunStartTests
Assert.True(FireControl.InArc(plight, toWaiting), "the waiting Djinni is inside plight's arc");
Assert.True(length(toWaiting) <= range, "the waiting Djinni is inside plight's Range");
Assert.True(length(toWaiting) > .75f * range, "the waiting Djinni is beyond three quarters of plight's Range");
// The crossing is what the sim does with that staging, read in the player's frame: the arena's gravity and the
// crosser's pilot act from the first tick, so the t=0 facing proves nothing. Within CrossingSeconds the crosser's
// bearing sweeps from port to starboard while it is ahead and inside CrossingMetres, and both guns bear on it.
var swept = false;
for (var tick = 0; tick < 60 * CrossingSeconds && !swept; tick++)
{
var before = crosser.Position - player.Position;
arena.Update(1f / 60f);
var after = crosser.Position - player.Position;
if (before.x < 0 && after.x >= 0)
{
swept = true;
Assert.True(after.z > 0, "the crosser passes the bow line ahead of the player, not astern");
Assert.True(length(after) < CrossingMetres, "the crosser passes inside the stated distance");
Assert.True(FireControl.InArc(small, after), "FastBlast+- bears on the crosser as it crosses");
}
}
Assert.True(swept, $"the crosser's bearing never swept across the bow within {CrossingSeconds} s");
}
private const int CrossingSeconds = 6;
private const float CrossingMetres = 150f;
// Djinni Shakedown: the generated Djinni carries a gun on a Ballistic hardpoint (not a launcher); that mount bears on
// Ahead and not on Abeam, and both targets are neutral.
[Fact]
@ -66,7 +90,13 @@ public sealed partial class RunStartTests
Assert.Equal(2, staged.Entities.Count);
var ahead = staged.Entities[0];
var abeam = staged.Entities[1];
Assert.Equal((DjinniShakedown.Ahead, DjinniShakedown.Abeam), (ahead.Position.xz, abeam.Position.xz));
var facing = player.Direction;
var perpendicular = new float2(facing.y, -facing.x);
Assert.Equal(400f, length(ahead.Position.xz), .01f);
Assert.Equal(400f, length(abeam.Position.xz), .01f);
Assert.Equal(1f, dot(normalize(ahead.Position.xz), facing), .001f);
Assert.Equal(1f, abs(dot(normalize(abeam.Position.xz), perpendicular)), .001f);
Assert.Equal(("Longinus", "Longinus"), (_items.GetData(ahead.Hull).Name, _items.GetData(abeam.Hull).Name));
foreach (var barrel in barrels)
{
Assert.True(FireControl.InArc(barrel, ahead.Position - player.Position), $"{barrel.Data.Name} cannot bear on Ahead");