Pursuit scenario: Cargo stages consumables; a hostile on your tail, a Mine Launcher fitted, vents and overdrives in the hold

ScenarioStage.Cargo resolves one name across equippable and consumable
designs; a consumable is built from its first available product in key
order. Pursuit (seed 111, demo cast) lists last in Scenarios.Tests.
All commits build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 18:41:48 +02:00
commit 9ccc846c1a
6 changed files with 192 additions and 8 deletions

View file

@ -190,7 +190,7 @@ public static class Loadouts
return () => (EquippableItem) itemManager.CreateInstance(product);
}
private static FactionProductData[] ProductsInKeyOrder(CultCache cache) =>
internal static FactionProductData[] ProductsInKeyOrder(CultCache cache) =>
cache.GetAll<FactionProductData>().OrderBy(p => cache.RefOf(p).Key.Value, StringComparer.Ordinal).ToArray();
private static string Cell(LoadoutSlot slot) => $"slot {slot.Position.x},{slot.Position.y}";

View file

@ -116,7 +116,9 @@ public sealed class ScenarioStage
return entity;
}
// Items in the entity's first cargo bay, one per design named.
// Items in the entity's first cargo bay, one per design named. A name is an equippable design (built as a fit
// builds it) or a consumable design (built from its first available product in key order, as a generated hold is
// stocked); a name that is neither is a failure.
public void Cargo(Entity entity, params string[] designs)
{
if (entity == null) return;
@ -124,10 +126,27 @@ public sealed class ScenarioStage
var bay = entity.CargoBays.FirstOrDefault();
foreach (var design in designs)
{
var data = Named<EquippableItemData>(design, where);
var data = Named<CraftedItemData>(design, where, found => found is EquippableItemData || found is ConsumableItemData);
if (data == null) continue;
var build = Loadouts.Resolve(_items, _items.ItemData.RefOf(data), _available, where, _failures);
if (build != null && (bay == null || !bay.TryStore(build())))
Func<CraftedItemInstance> build;
if (data is ConsumableItemData)
{
var key = _items.ItemData.RefOf(data).Key;
var product = Loadouts.ProductsInKeyOrder(_items.ItemData).Where(p => p.Design.Key.Equals(key)).FirstOrDefault(p => _available(p));
if (product == null)
{
_failures.Add($"{where}: no product offers {design}");
continue;
}
build = () => _items.CreateInstance(product);
}
else
{
var equippable = Loadouts.Resolve(_items, _items.ItemData.RefOf((EquippableItemData) data), _available, where, _failures);
if (equippable == null) continue;
build = equippable;
}
if (bay == null || !bay.TryStore(build()))
_failures.Add($"{where}: no room for {design}");
}
}
@ -151,9 +170,9 @@ public sealed class ScenarioStage
return entity;
}
private T Named<T>(string name, string where) where T : EquippableItemData
private T Named<T>(string name, string where, Func<T, bool> among = null) where T : CraftedItemData
{
var named = _items.ItemData.GetAll<T>().Where(data => data.Name == name).Take(2).ToArray();
var named = _items.ItemData.GetAll<T>().Where(data => data.Name == name && (among == null || among(data))).Take(2).ToArray();
if (named.Length == 1) return named[0];
_failures.Add(named.Length == 0 ? $"{where}: no design named {name}" : $"{where}: more than one design is named {name}");
return null;

View file

@ -0,0 +1,60 @@
using static CultMath.math;
using static ItemRotation;
public sealed class Pursuit : Scenario
{
// The pursuer starts astern, inside the 400 at which VapourCloudTests.ACloudBreaksALockAtRange shows a fresh vent
// cloud breaking a sensor's lock, and well outside the 40 at which AStrongCloseSensorBurnsThrough shows a close
// sensor burning through.
private const float Astern = 300;
public override string Name => "Pursuit";
public override string Brief => "A hostile Longinus on your tail; a Mine Launcher fitted, three Coolant Vents and two Thruster " +
"Overdrives aboard. Verify: lay mines in its path, the mine pulses slowly after 2 s, fast when " +
"the hostile is near, explodes 2 s later; vent and coast to lose it; overdrive away.";
public override uint Seed => 111;
public override bool Ambient => false;
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(DemoTerminus.Cast);
public override void Stage(ScenarioStage stage)
{
var layer = 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),
("Mine Launcher", int2(0, 5), None),
("pswarm", int2(5, 5), None),
("FastBlast+-", 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),
("Store-All Plus", int2(2, 8), None));
var pursuer = 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),
("FastBlast+-", 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),
("PotaT+-", int2(1, 5), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Store-All Plus", int2(2, 8), None));
var player = stage.Player(layer, float2(0, 0), facing: float2(0, 1));
stage.Cargo(player, "Coolant Vent", "Coolant Vent", "Coolant Vent", "Thruster Overdrive", "Thruster Overdrive");
stage.Place(pursuer, float2(0, -Astern), facing: float2(0, 1), stance: ScenarioStance.Hostile, piloted: true);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b0587057e6a4618bf63944364298870
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

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