Merge origin/eureka/aetheria-gravity-potential-owner (cc3249b8) into grid-lowering
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
0d7fc2ee3a
2 changed files with 78 additions and 29 deletions
|
|
@ -4,8 +4,10 @@ using static ItemRotation;
|
|||
public sealed class Crossing : Scenario
|
||||
{
|
||||
public override string Name => "Crossing";
|
||||
public override string Brief => "A Lightning Projector at the starboard gun and a Cell Blaster at the port gun, a fast Djinni crossing close ahead and an " +
|
||||
"unpiloted one waiting at range. The Djinni starts to port moving across the bow, and its pilot brakes it as it closes. Make the pass coasting: give no throttle until the Djinni has crossed the bow, then fly freely (thrusting from the start carries you past it). Verify: as the Djinni crosses close, FastBlast lands some and projector " +
|
||||
public override string Brief => "A Lightning Projector at the starboard gun and a Cell Blaster at the port gun, an unpiloted Djinni coasting across the bow " +
|
||||
"close ahead and an unpiloted one waiting at range. The crosser is launched to port at 50 m/s and only coasts: nothing steers it, " +
|
||||
"its hull drag slows it to about 25 m/s at the bow line, and it does not shoot back. Make the pass coasting: give no throttle until the Djinni has crossed the bow, then fly freely " +
|
||||
"(thrusting from the start carries you past it). Verify: as the Djinni crosses close, FastBlast lands some and projector " +
|
||||
"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;
|
||||
|
|
@ -13,12 +15,13 @@ public sealed class Crossing : Scenario
|
|||
|
||||
// Where the Djinni sit: the player is at Origin facing +z. Origin is 250 m or more from every body, where the zone's
|
||||
// pull is weakest (about 1.4 u/s^2; the sum of the wells never drops to nothing, and at the old origin the player sat on the star).
|
||||
// The crosser starts ahead and to port, facing across the bow and already moving at the Djinni's 50 m/s cap; its pilot
|
||||
// brakes it as it closes, so it only just reaches the bow line, which fixes how far to port it starts (sweeping the starting
|
||||
// x from -95 to -97.5 moves the pass from 3.0 s to 3.5 s; at -98 it never crosses). The waiting Djinni sits inside the Lightning
|
||||
// Projector's arc at 80% of its Range.
|
||||
// The crosser is unpiloted and placed moving at the Djinni's 50 m/s cap, so its pass is ballistic: bent by the field and slowed
|
||||
// by hull drag (it reaches the bow line at about 25 m/s relative to the player, 3.6 s in), and nothing steers it.
|
||||
// CrosserAt is the centre of the start window in which the pass holds: shifting it by 10 m along either axis keeps the pass
|
||||
// inside the test's time and distance windows (CrossingHoldsAcrossTheStartWindow).
|
||||
// The waiting Djinni sits inside the Lightning Projector's arc at 80% of its Range.
|
||||
public static readonly CultMath.float2 Origin = float2(724, 124);
|
||||
public static readonly CultMath.float2 CrosserAt = Origin + float2(-96.5f, 70), CrosserFacing = float2(1, 0), CrosserVelocity = float2(50, 0);
|
||||
public static readonly CultMath.float2 CrosserAt = Origin + float2(-128, 45), CrosserFacing = float2(1, 0), CrosserVelocity = float2(50, 0);
|
||||
public const float ProjectorRange = 1216f; // the Lightning Projector's Range in metres, 1216.4 as the fit stands settled at rest
|
||||
public static readonly CultMath.float2 Waiting = Origin + float2(0, ProjectorRange * .8f);
|
||||
|
||||
|
|
@ -43,7 +46,7 @@ public sealed class Crossing : Scenario
|
|||
("Fire Control Array", int2(1, 6), Clockwise));
|
||||
|
||||
stage.Player(fighter, Origin, facing: float2(0, 1));
|
||||
stage.Place(stage.Generated("Djinni"), CrosserAt, facing: CrosserFacing, stance: ScenarioStance.Hostile, piloted: true, velocity: CrosserVelocity);
|
||||
stage.Place(stage.Generated("Djinni"), CrosserAt, facing: CrosserFacing, stance: ScenarioStance.Hostile, velocity: CrosserVelocity);
|
||||
stage.Place(stage.Bare("Djinni"), Waiting);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ using float3 = CultMath.float3;
|
|||
public sealed partial class RunStartTests
|
||||
{
|
||||
// Crossing: the demo's cast; projector and Cell Blaster fitted in separate weapon groups with the Fire Control Array
|
||||
// aboard; a piloted hostile Djinni that crosses the bow inside 400 m; and an unpiloted neutral one inside the Lightning Projector's
|
||||
// aboard; an unpiloted hostile Djinni, coasting at the Djinni's speed cap, that crosses the bow inside 400 m; and an unpiloted neutral one inside the Lightning Projector's
|
||||
// arc, short of its Range and beyond three quarters of it.
|
||||
[Fact]
|
||||
public void CrossingConditions()
|
||||
|
|
@ -37,7 +37,7 @@ public sealed partial class RunStartTests
|
|||
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));
|
||||
Assert.Equal(0, AgentsOf(arena, crosser));
|
||||
var toCrosser = crosser.Position - player.Position;
|
||||
Assert.True(length(toCrosser) < 400f, "the crosser starts inside 400 m");
|
||||
Assert.True(toCrosser.z > 0, "the crosser starts ahead");
|
||||
|
|
@ -59,34 +59,79 @@ public sealed partial class RunStartTests
|
|||
Assert.True(length(toWaiting) <= range, "the waiting Djinni is inside the Lightning Projector's Range");
|
||||
Assert.True(length(toWaiting) > .75f * range, "the waiting Djinni is beyond three quarters of the Lightning Projector'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.
|
||||
// The pass is made by a coasting player: the loop gives no input, and the Brief tells the operator to fly it that way.
|
||||
// The crossing is what the sim does with that staging, read in the player's frame: the arena's gravity acts 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. The pass is made by a coasting
|
||||
// player: the loop gives no input, and the Brief tells the operator to fly it that way.
|
||||
Assert.Equal(float2.zero, player.MovementDirection);
|
||||
var swept = false;
|
||||
var crossedAt = 0f;
|
||||
var crossedMetres = 0f;
|
||||
for (var tick = 0; tick < 60 * CrossingSeconds && !swept; tick++)
|
||||
var pass = FlyPass(arena, player, crosser);
|
||||
Assert.True(pass.Swept, $"the crosser's bearing never swept across the bow within {CrossingSeconds} s");
|
||||
Assert.True(pass.Ahead.y > 0, "the crosser passes the bow line ahead of the player, not astern");
|
||||
Assert.True(length(pass.Ahead) < CrossingMetres, "the crosser passes inside the stated distance");
|
||||
Assert.True(FireControl.InArc(small, float3(pass.Ahead.x, 0, pass.Ahead.y)), "Cell Blaster bears on the crosser as it crosses");
|
||||
Assert.True(dot(FireControl.MountDirection(small), float3(player.Direction.x, 0, player.Direction.y)) > 0, "Cell Blaster mounts forward");
|
||||
// At seed 112 with no input the pass is at about 3.6 s and 45 m; a seed, field or staging change that loses it moves these.
|
||||
Assert.InRange(pass.Seconds, CrossingEarliest, CrossingLatest);
|
||||
Assert.InRange(length(pass.Ahead), 30f, 60f);
|
||||
}
|
||||
|
||||
// The unpiloted crosser's pass is ballistic, bent only by the field, so it does not hang on a lucky start: shifting where
|
||||
// the crosser starts by up to 10 m along either axis keeps the pass inside the test's time and distance windows, fast across
|
||||
// the line (relative speed at the bow line) and sweeping the player's view at a steady rate (angular rate seen from the player).
|
||||
// The shifts are applied to what Crossing stages, so the window is the scenario's own.
|
||||
[Fact]
|
||||
public void CrossingHoldsAcrossTheStartWindow()
|
||||
{
|
||||
var passes = new System.Collections.Generic.List<(float2 shift, CrossingPass pass)>();
|
||||
foreach (var along in new[] { true, false })
|
||||
for (var step = -4; step <= 4; step++)
|
||||
{
|
||||
var shift = along ? float2(step * 2.5f, 0) : float2(0, step * 2.5f);
|
||||
var (_, arena, staged, failures) = Launch(new Crossing(), Inputs(() => GalaxySeed));
|
||||
Assert.True(failures.Count == 0, string.Join("; ", failures));
|
||||
var crosser = staged.Entities[0];
|
||||
crosser.Position += float3(shift.x, 0, shift.y);
|
||||
passes.Add((shift, FlyPass(arena, staged.Player, crosser)));
|
||||
}
|
||||
foreach (var (shift, pass) in passes)
|
||||
{
|
||||
var at = $"crosser start shifted by ({shift.x}, {shift.y}) m";
|
||||
Assert.True(pass.Swept, $"{at}: never crosses the bow within {CrossingSeconds} s");
|
||||
Assert.True(pass.Ahead.y > 0 && length(pass.Ahead) < CrossingMetres, $"{at}: crosses astern or beyond {CrossingMetres} m");
|
||||
Assert.InRange(pass.Seconds, CrossingEarliest, CrossingLatest);
|
||||
Assert.InRange(length(pass.Ahead), 30f, 60f);
|
||||
Assert.InRange(pass.RelativeSpeed, 20f, 30f);
|
||||
Assert.InRange(pass.AngularRate, 0.3f, 0.7f);
|
||||
}
|
||||
|
||||
// The shifts took effect: starting 20 m further out along the crosser's track delays the pass, and starting 20 m further
|
||||
// ahead moves it 20 m further ahead (a sweep that stages the same pass eighteen times proves nothing).
|
||||
var outward = passes.Take(9).Select(entry => entry.pass).ToList();
|
||||
var lateral = passes.Skip(9).Select(entry => entry.pass).ToList();
|
||||
Assert.True(outward[0].Seconds - outward[8].Seconds > .5f, "a start 20 m further out delays the pass by more than half a second");
|
||||
Assert.InRange(lateral[8].Ahead.y - lateral[0].Ahead.y, 3f, 30f);
|
||||
}
|
||||
|
||||
// Where the crosser's bearing from the player sweeps from port to starboard: when, where it is then (x port-to-starboard,
|
||||
// y ahead), its speed relative to the player and the angular rate of its bearing in radians per second.
|
||||
private readonly record struct CrossingPass(bool Swept, float Seconds, float2 Ahead, float RelativeSpeed, float AngularRate);
|
||||
|
||||
private static CrossingPass FlyPass(Zone arena, Ship player, Entity crosser)
|
||||
{
|
||||
for (var tick = 0; tick < 60 * CrossingSeconds; 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;
|
||||
crossedAt = (tick + 1) / 60f;
|
||||
crossedMetres = length(after);
|
||||
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), "Cell Blaster bears on the crosser as it crosses");
|
||||
Assert.True(dot(FireControl.MountDirection(small), float3(player.Direction.x, 0, player.Direction.y)) > 0, "Cell Blaster mounts forward");
|
||||
var at = float2(after.x, after.z);
|
||||
var relative = crosser.Velocity - player.Velocity;
|
||||
return new CrossingPass(true, (tick + 1) / 60f, at, length(relative), abs(at.x * relative.y - at.y * relative.x) / dot(at, at));
|
||||
}
|
||||
}
|
||||
Assert.True(swept, $"the crosser's bearing never swept across the bow within {CrossingSeconds} s");
|
||||
// At seed 112 with no input the pass is at 3.2 s and 40 m (the pilot brakes the crosser as it closes, so the pass is sensitive to where it starts: Crossing.CrosserAt); a seed, field or staging change that loses it moves these.
|
||||
Assert.InRange(crossedAt, 3.0f, 4.2f);
|
||||
Assert.InRange(crossedMetres, 30f, 60f);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
// Place with no velocity leaves the entity at rest: the unpiloted waiting Djinni and both Shakedown targets start with
|
||||
|
|
@ -104,6 +149,7 @@ public sealed partial class RunStartTests
|
|||
|
||||
private const int CrossingSeconds = 6;
|
||||
private const float CrossingMetres = 150f;
|
||||
private const float CrossingEarliest = 3.0f, CrossingLatest = 4.2f;
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue