From 7ab267c314abc2780b7fa1c1a0d8a281b45044ed Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 11 Oct 2026 18:52:35 +0200 Subject: [PATCH] Crossing scenario and Djinni Shakedown targets, with their staging tests Crossing (seed 112, demo cast): the Arcs Longinus fit with plight and FastBlast+- under the Fire Control Array, a piloted hostile Djinni crossing the bow inside 400 m, an unpiloted neutral one at 80% of plight's Range. Djinni Shakedown gains bare Longinus targets Ahead (inside the generated Djinni's ballistic mount arc) and Abeam (outside it). CrossingConditions and DjinniShakedownTargets read what each arena staged. Not yet verified on a full-suite run at this commit. Co-Authored-By: Claude Opus 5.5 --- .../ServerShared/Scenarios/Crossing.cs | 44 +++++++++++ .../ServerShared/Scenarios/Crossing.cs.meta | 11 +++ .../ServerShared/Scenarios/DjinniShakedown.cs | 15 +++- .../ServerShared/Scenarios/Scenarios.cs | 2 +- .../CrossingScenarioTests.cs | 78 +++++++++++++++++++ 5 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 Assets/Scripts/ServerShared/Scenarios/Crossing.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/Crossing.cs.meta create mode 100644 tests/Aetheria.Shared.Tests/CrossingScenarioTests.cs diff --git a/Assets/Scripts/ServerShared/Scenarios/Crossing.cs b/Assets/Scripts/ServerShared/Scenarios/Crossing.cs new file mode 100644 index 000000000..82e89f1c4 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/Crossing.cs @@ -0,0 +1,44 @@ +using static CultMath.math; +using static ItemRotation; + +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 " + + "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); + 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); + + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(DemoTerminus.Cast); + + public override void Stage(ScenarioStage stage) + { + var fighter = stage.Fit("Longinus", + ("Cockpit 2x2", int2(2, 6), None), + ("Large Drive", int2(1, 0), Reversed), + ("Large Drive", int2(3, 0), Reversed), + ("Talaria", int2(2, 14), CounterClockwise), + ("Talaria", int2(3, 14), Clockwise), + ("Core Power", int2(2, 4), None), + ("GT 3K", int2(0, 5), None), + ("GT 3K", int2(5, 5), None), + ("plight", int2(1, 8), None), + ("FastBlast+-", int2(4, 8), None), + ("Iapyx", int2(2, 2), CounterClockwise), + ("Iapyx", int2(3, 2), Clockwise), + ("not if i see you first", int2(3, 10), None), + ("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.Bare("Djinni"), Waiting); + } +} diff --git a/Assets/Scripts/ServerShared/Scenarios/Crossing.cs.meta b/Assets/Scripts/ServerShared/Scenarios/Crossing.cs.meta new file mode 100644 index 000000000..2787758ab --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/Crossing.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0cc3edfb4b0e4ea485cce71354f41748 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs b/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs index 720fa2d08..7cbec5630 100644 --- a/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs +++ b/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs @@ -3,13 +3,22 @@ using float2 = CultMath.float2; public sealed class DjinniShakedown : Scenario { public override string Name => "Djinni Shakedown"; - public override string Brief => "A fitted Djinni in empty space. Verify: its thrusters move and turn it; with the hull " + - "offline and thrusters firing, its speed does not run away."; + public override string Brief => "A fitted Djinni in empty space, facing +z, with a bare hull dead ahead and one abeam. Verify: its thrusters " + + "move and turn it; with the hull offline and thrusters firing, its speed does not run away. Its " + + "articulated ballistic barrel points at the hull ahead when it bears, and rests at the arc edge while you " + + "aim at the hull abeam."; public override uint Seed => 101; public override bool Ambient => false; + // Where the targets sit: the ballistic mount faces +z, so Ahead is inside its arc and Abeam outside it. + public static readonly float2 Ahead = new float2(0, 400), Abeam = new float2(400, 0); + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(); - public override void Stage(ScenarioStage stage) => + public override void Stage(ScenarioStage stage) + { stage.Player(stage.Generated("Djinni"), float2.zero); + stage.Place(stage.Bare("Longinus"), Ahead); + stage.Place(stage.Bare("Longinus"), Abeam); + } } diff --git a/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs b/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs index 4a43c8960..b6ad3a365 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs @@ -9,6 +9,6 @@ public static class Scenarios public static readonly Scenario[] Tests = { - new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new LongHaul() + new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new LongHaul(), new Crossing() }; } diff --git a/tests/Aetheria.Shared.Tests/CrossingScenarioTests.cs b/tests/Aetheria.Shared.Tests/CrossingScenarioTests.cs new file mode 100644 index 000000000..3bca80972 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/CrossingScenarioTests.cs @@ -0,0 +1,78 @@ +using System.Linq; +using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; + +// The arenas the operator flies to check the evasion term and the articulated barrel (map: Scenarios for operator +// checks). Each test launches the scenario the way New Game does and reads what it staged, so a script that stops +// staging what its brief claims fails here. +public sealed partial class RunStartTests +{ + // Crossing: the demo's cast; plight and FastBlast+- 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 plight's + // arc, short of its Range and beyond three quarters of it. + [Fact] + public void CrossingConditions() + { + var (galaxy, arena, staged, failures) = Launch(new Crossing(), Inputs(() => GalaxySeed)); + Assert.True(failures.Count == 0, string.Join("; ", failures)); + Assert.Equal(DemoTerminus.Cast.ZoneCount, galaxy.Zones.Length); + Assert.Equal(4, galaxy.HomeZones.Count); + + var player = staged.Player; + var plight = player.Equipment.Single(item => item.Data.Name == "plight"); + var small = player.Equipment.Single(item => item.Data.Name == "FastBlast+-"); + Assert.Contains(player.Equipment, item => item.Data.Name == "Fire Control Array"); + var groupOf = new System.Func(item => System.Array.FindIndex(player.WeaponGroups, group => group.items.Contains(item))); + Assert.True(groupOf(plight) >= 0 && groupOf(small) >= 0, "both guns are in a weapon group"); + Assert.NotEqual(groupOf(plight), groupOf(small)); + + Assert.Equal(2, staged.Entities.Count); + var crosser = staged.Entities[0]; + var waiting = staged.Entities[1]; + Assert.IsType(crosser); + Assert.IsType(waiting); + + Assert.True(player.IsHostileTo(crosser) && crosser.IsHostileTo(player), "the crosser is hostile both ways"); + Assert.Equal(1, 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"); + Assert.True(abs(dot(crosser.Direction, player.Direction)) <= sin(radians(30f)) + 1e-4f, + "the crosser faces within 30 degrees of perpendicular to the player's bow"); + + Assert.False(player.IsHostileTo(waiting) || waiting.IsHostileTo(player), "the waiting Djinni is neutral"); + Assert.Equal(0, AgentsOf(arena, waiting)); + var toWaiting = waiting.Position - player.Position; + EvSettle(player); // a weapon's Range is read once the ship's stats have settled; the geometry above is as staged + var range = plight.Behaviors.OfType().Single().Range; + 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"); + } + + // 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] + public void DjinniShakedownTargets() + { + var (_, _, staged, failures) = Launch(new DjinniShakedown(), Inputs(() => GalaxySeed)); + Assert.True(failures.Count == 0, string.Join("; ", failures)); + var player = staged.Player; + var barrels = player.Equipment.Where(item => item.Hardpoint?.Type == HardpointType.Ballistic + && item.Behaviors.OfType().Any(weapon => !(weapon.WeaponData is LauncherData))).ToList(); + Assert.NotEmpty(barrels); + + 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)); + foreach (var barrel in barrels) + { + Assert.True(FireControl.InArc(barrel, ahead.Position - player.Position), $"{barrel.Data.Name} cannot bear on Ahead"); + Assert.False(FireControl.InArc(barrel, abeam.Position - player.Position), $"{barrel.Data.Name} bears on Abeam"); + } + Assert.False(player.IsHostileTo(ahead) || ahead.IsHostileTo(player) || player.IsHostileTo(abeam) || abeam.IsHostileTo(player), + "both targets are neutral"); + } +}