Merge eureka/aetheria-release-product-lines-weapons (cut-product-lines-weapons r5, h4) into master

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-17 04:48:14 +02:00
commit 6179846249
28 changed files with 1032 additions and 216 deletions

View file

@ -6,9 +6,10 @@ using GameCult.Caching;
using MessagePack;
using CultMath;
// A ship preset (a variant): item designs only, the hull's design, and per hull cell a design and its rotation. A design
// names no manufacturer; which product builds a slot is a fact of the galaxy the preset is materialized in, which keeps
// a preset portable across galaxies. Authored catalog data: written in CultCache Studio or by the editor capture
// A ship preset (a variant): the hull's design, and per hull cell a design and its rotation. A slot names a design and
// may name a product of it; a design-only slot is open on purpose, and whoever materializes the preset chooses its
// product from the galaxy it is built in (ruling loadout-product-optional-context-chooses), which keeps a preset
// portable across galaxies. A preset that needs a brand names it. Authored catalog data: written in CultCache Studio or by the editor capture
// command, read at runtime.
[CultDocument("aetheria.loadout", "1"), MessagePackObject]
public class Loadout
@ -17,6 +18,7 @@ public class Loadout
[Key(1)] public CultRecordRef<HullData> Hull;
[Key(2)] public List<LoadoutSlot> Slots = new List<LoadoutSlot>();
[Key(3)] public int[][] WeaponGroups; // indices into Slots
[Key(4)] public CultRecordRef<FactionProductData> HullProduct; // unset: the hull's design alone, chosen at materialization
}
[MessagePackObject]
@ -25,14 +27,15 @@ public class LoadoutSlot
[Key(0)] public int2 Position; // hull cell passed to Entity.TryEquip(item, int2)
[Key(1)] public ItemRotation Rotation;
[Key(2)] public CultRecordRef<EquippableItemData> Design;
[Key(3)] public CultRecordRef<FactionProductData> Product; // unset: design-only, chosen at materialization
}
// The owner of loadouts: capture from a live ship, the only writer, and the only builder of an entity from a loadout.
public static class Loadouts
{
// Records the hull design and one slot per equipped item (gear, then cargo bays, then docking bays) with its cell,
// rotation and design. Entity.Equipment also holds the hull's own unit; that is Hull, not a slot. Nothing about the
// units themselves: no product, quality, ingredients, cargo or state.
// rotation, design and product (unset for an unbranded unit). Entity.Equipment also holds the hull's own unit; that is
// Hull, not a slot. Nothing else about the units: no quality, ingredients, cargo or state.
public static Loadout Capture(ItemManager itemManager, Entity entity, string name)
{
var cache = itemManager.ItemData;
@ -46,11 +49,13 @@ public static class Loadouts
{
Name = name,
Hull = cache.RefOf((HullData) itemManager.GetData(entity.Hull)),
HullProduct = ProductRef(itemManager, entity.Hull),
Slots = items.Select(item => new LoadoutSlot
{
Position = item.Position,
Rotation = item.EquippableItem.Rotation,
Design = cache.RefOf(itemManager.GetData(item.EquippableItem))
Design = cache.RefOf(itemManager.GetData(item.EquippableItem)),
Product = ProductRef(itemManager, item.EquippableItem)
}).ToList(),
WeaponGroups = entity.WeaponGroups
.Select(group => group.items.Select(item => items.IndexOf(item)).ToArray())
@ -58,6 +63,9 @@ public static class Loadouts
};
}
private static CultRecordRef<FactionProductData> ProductRef(ItemManager itemManager, CraftedItemInstance unit) =>
itemManager.Brand(unit).Product is { } product ? itemManager.ItemData.RefOf(product) : default;
// A preset's key derives from its name, so capturing the same name again addresses the same record.
public static CultRecordKey KeyOf(string name) => new CultRecordKey("loadout:" + name);
@ -95,24 +103,23 @@ public static class Loadouts
// All-or-nothing: returns an entity only when every design resolved and fitted, the loadout has at most
// WeaponGroupCount weapon groups, and every group index names a weapon slot. Every failure is listed; on any failure
// nothing is returned and the zone and cache are unchanged, but a failed fit has already drawn from
// itemManager.Random. How each design is built is Resolve's rule. A game spawner is to pass
// LoadoutGenerator.IsAvailable as isAvailable, with no fallback to any manufacturer; RunStart does. The hull's type
// itemManager.Random. How each design is built is Resolve's rule. A design-only slot is chosen by choose; a game
// spawner is to pass LoadoutGenerator.ChooseProduct, with no fallback to any manufacturer; RunStart does. The hull's type
// picks the entity: a ship hull builds a Ship, a turret hull an OrbitalEntity with no orbit, which stays where it is
// put; a station hull is refused. The entity always gets exactly WeaponGroupCount groups; a loadout with fewer is
// padded with empty groups. A failed materialization leaves any minted lots in the live ledger; they are not roots,
// so the next commit drops them.
public static Entity Materialize(ItemManager itemManager, Zone liveZone, Loadout loadout,
Predicate<FactionProductData> isAvailable, List<string> failures)
Func<CraftedItemData, FactionProductData> choose, List<string> failures)
{
var cache = itemManager.ItemData;
var products = ProductsInKeyOrder(cache);
var reported = failures.Count;
var hullBuild = Resolve(itemManager, products, loadout.Hull, isAvailable, "hull", failures);
var hullBuild = Resolve(itemManager, loadout.Hull, loadout.HullProduct, choose, "hull", failures);
var hullData = cache.Get(loadout.Hull);
if (hullData is { HullType: HullType.Station })
failures.Add($"hull: {hullData.Name} is a station hull; a preset builds a ship or a turret");
var slotBuilds = loadout.Slots.Select(slot => Resolve(itemManager, products, slot.Design, isAvailable, Cell(slot), failures)).ToArray();
var slotBuilds = loadout.Slots.Select(slot => Resolve(itemManager, slot.Design, slot.Product, choose, Cell(slot), failures)).ToArray();
var groups = loadout.WeaponGroups ?? new int[0][];
var groupCount = itemManager.GameplaySettings.WeaponGroupCount;
if (groups.Length > groupCount) failures.Add($"weapon groups: {groups.Length} groups, at most {groupCount}");
@ -124,7 +131,7 @@ public static class Loadouts
}
if (failures.Count > reported) return null;
var hull = hullBuild();
var hull = (EquippableItem) hullBuild();
var settings = itemManager.GameplaySettings.DefaultEntitySettings;
Entity entity = hullData.HullType == HullType.Turret
? new OrbitalEntity(itemManager, liveZone, hull, default, settings)
@ -133,7 +140,7 @@ public static class Loadouts
for (var i = 0; i < loadout.Slots.Count; i++)
{
var slot = loadout.Slots[i];
var unit = slotBuilds[i]();
var unit = (EquippableItem) slotBuilds[i]();
unit.Rotation = slot.Rotation;
if (!entity.TryEquip(unit, slot.Position))
{
@ -159,39 +166,51 @@ public static class Loadouts
}
// How one design is built, or null with the failure listed; nothing is minted until the returned builder runs. A
// design is built by its first available product in record-key order. A design with no product at all is outside
// the economy (docs/scenarios-cut.md, Q3: generation, stock and sale all go through products) and is built
// unbranded, with no maker, at UnbrandedQuality. A design whose products are all unavailable is not built.
public static Func<EquippableItem> Resolve<T>(ItemManager itemManager, CultRecordRef<T> design,
Predicate<FactionProductData> isAvailable, string where, List<string> failures) where T : EquippableItemData =>
Resolve(itemManager, ProductsInKeyOrder(itemManager.ItemData), design, isAvailable, where, failures);
private static Func<EquippableItem> Resolve<T>(ItemManager itemManager, FactionProductData[] products,
CultRecordRef<T> design, Predicate<FactionProductData> isAvailable, string where, List<string> failures)
where T : EquippableItemData
// pinned product is built as named, whatever choose says; it must be a product of the design. A design with no
// product at all is outside the economy (docs/scenarios-cut.md, Q3: generation, stock and sale all go through
// products) and is built unbranded, with no maker, at UnbrandedQuality. Otherwise choose names the product, and
// null (nothing available) or a product of another design is a failure.
public static Func<CraftedItemInstance> Resolve<T>(ItemManager itemManager, CultRecordRef<T> design,
CultRecordRef<FactionProductData> product, Func<CraftedItemData, FactionProductData> choose, string where,
List<string> failures) where T : CraftedItemData
{
var data = itemManager.ItemData.Get(design);
var cache = itemManager.ItemData;
var data = cache.Get(design);
if (data == null)
{
failures.Add($"{where}: design {design.Key} is not in the catalog");
return null;
}
var made = products.Where(p => p.Design.Key.Equals(design.Key)).ToArray();
if (made.Length == 0)
return () => (EquippableItem) itemManager.CreateInstance(itemManager.CreateLot(data, default, UnbrandedQuality));
var product = made.FirstOrDefault(p => isAvailable(p));
if (product == null)
FactionProductData made;
if (product.IsSet())
{
failures.Add($"{where}: no available product of {data.Name}");
made = cache.Get(product);
if (made == null)
{
failures.Add($"{where}: product {product.Key} is not in the catalog");
return null;
}
}
else
{
if (!cache.GetAll<FactionProductData>().Any(p => p.Design.Key.Equals(design.Key)))
return () => itemManager.CreateInstance(itemManager.CreateLot(data, default, UnbrandedQuality));
made = choose(data);
if (made == null)
{
failures.Add($"{where}: no available product of {data.Name}");
return null;
}
}
if (!made.Design.Key.Equals(design.Key))
{
failures.Add($"{where}: product {made.Name} is not a product of {data.Name}");
return null;
}
return () => (EquippableItem) itemManager.CreateInstance(product);
return () => itemManager.CreateInstance(made);
}
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

@ -177,7 +177,7 @@ public class LoadoutGenerator
{
var (available, preferManufacturers) = AvailableProducts(filter, required);
return available.WeightedRandomElements(ref Random, entry =>
(preferManufacturers ? ManufacturerPreference(entry.product.Manufacturer) : 1) *
(preferManufacturers ? ProductWeight(entry.product) : 1) *
pow(entry.design.Shape.Coordinates.Length, sizeExponent) / // Prioritize larger items
pow(entry.design.Price, PriceExponent), // Penalize item price to a controllable degree
count);
@ -243,8 +243,8 @@ public class LoadoutGenerator
// No galaxy, or no faction, means no allegiance to filter by: every product is on offer. A fixture generates
// loadouts that way, so a test can exercise placement and products without standing up a whole galaxy; a scenario
// stage has no faction and reaches every maker. Loadouts.Materialize takes availability as a predicate; RunStart,
// the game's one preset spawner, passes this, so presets and generation share one availability rule. A faction
// stage has no faction and reaches every maker. Loadouts.Materialize takes a chooser and ChooseProduct is the game's,
// so presets and generation share this one availability rule. A faction
// always reaches its own manufacturer's gear, and otherwise gear made by a manufacturer its allegiance names
// (operator, 2026-09-30: allegiance lists only other factions). Whether that manufacturer is present in the galaxy
// does not matter (operator, 2026-10-06: a faction need not be present for its gear to be available).
@ -252,6 +252,25 @@ public class LoadoutGenerator
Galaxy == null || Faction == null ||
ItemManager.ItemData.Get(product.Manufacturer) == Faction || Faction.Allegiance.ContainsKey(product.Manufacturer);
// How much this outfitter favors a product: its maker's preference. The one weight of generation and of ChooseProduct.
public float ProductWeight(FactionProductData product) => ManufacturerPreference(product.Manufacturer);
// The product a design named without one is built as (ruling loadout-product-optional-context-chooses): among the
// products of the design this outfitter can get, one draw from the run's seeded Random weighted by ProductWeight;
// null when none, and uniform over them when every weight is 0. Candidates are ordered by name so the draw's
// enumeration, and so its result, never depends on record keys. All of the policy is this body.
public FactionProductData ChooseProduct(CraftedItemData design)
{
var key = ItemManager.ItemData.RefOf(design).Key;
var candidates = ItemManager.ItemData.GetAll<FactionProductData>()
.Where(product => product.Design.Key.Equals(key) && IsAvailable(product))
.OrderBy(product => product.Name, StringComparer.Ordinal)
.ToArray();
if (candidates.Length == 0) return null;
var uniform = candidates.All(product => ProductWeight(product) <= 0);
return candidates.WeightedRandomElements(ref Random, product => uniform ? 1 : ProductWeight(product), 1)[0];
}
// Prioritize products from the zone faction and its allies, penalizing distance to the manufacturer's headquarters.
// A manufacturer with no home zone in this galaxy weighs as if its headquarters lay one jump beyond the farthest
// zone this zone reaches: reachable through allegiance, least preferred.

View file

@ -9,11 +9,11 @@ using float3 = CultMath.float3;
// The run half of a scenario's vocabulary (docs/scenarios-cut.md, R.4). A scenario's Stage builds its fits and places
// them here; nothing joins the arena until Stage returns with no failure, and then RunStart admits it all at once.
// Every verb names what it could not do instead of throwing, so one bad name in a script lists every bad name.
// Designs and hulls are named as the catalog names them. Only RunStart builds a stage.
// Designs, hulls and products are named as the catalog names them. Only RunStart builds a stage.
public sealed class ScenarioStage
{
private readonly ItemManager _items;
private readonly Predicate<FactionProductData> _available;
private readonly Func<CraftedItemData, FactionProductData> _choose;
private readonly List<string> _failures = new List<string>();
private readonly List<Placement> _placed = new List<Placement>();
private int _placements;
@ -25,8 +25,9 @@ public sealed class ScenarioStage
StartingHull = startingHull;
TutorialGenerationSettings = tutorialGenerationSettings;
Credits = items.GameplaySettings.StartingCredits;
// A fit names designs, not manufacturers, so what the galaxy offers decides which product builds each one.
_available = new LoadoutGenerator(ref items.Random, items, arena.Galaxy, arena.GalaxyZone, null, 2).IsAvailable;
// A design named without a product is chosen by the generator's ChooseProduct. The stage outfits with no faction,
// so every maker is reachable and the run's seed draws.
_choose = new LoadoutGenerator(ref items.Random, items, arena.Galaxy, arena.GalaxyZone, null, 2).ChooseProduct;
}
public Galaxy Galaxy => Arena.Galaxy;
@ -49,18 +50,24 @@ public sealed class ScenarioStage
public bool Piloted;
}
// A hull fitted slot by slot: each slot a design, the hull cell it goes in, and its rotation.
// 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
// a design (open: the stage chooses its product) or a product (pinned: that brand).
public ScenarioFit Fit(string hull, params (string design, int2 cell, ItemRotation rotation)[] slots)
{
var where = $"fit {hull}";
var hullData = Named<HullData>(hull, where);
var (hullData, hullProduct) = NamedOrProduct<HullData>(hull, where);
var loadout = new Loadout { Name = hull, WeaponGroups = new int[0][] };
if (hullData != null) loadout.Hull = _items.ItemData.RefOf(hullData);
if (hullProduct != null) loadout.HullProduct = _items.ItemData.RefOf(hullProduct);
foreach (var (design, cell, rotation) in slots)
{
var data = Named<EquippableItemData>(design, where);
var (data, product) = NamedOrProduct<EquippableItemData>(design, where);
if (data == null) continue;
loadout.Slots.Add(new LoadoutSlot { Design = _items.ItemData.RefOf(data), Position = cell, Rotation = rotation });
loadout.Slots.Add(new LoadoutSlot
{
Design = _items.ItemData.RefOf(data), Position = cell, Rotation = rotation,
Product = product == null ? default : _items.ItemData.RefOf(product)
});
}
return hullData != null && loadout.Slots.Count == slots.Length ? new ScenarioFit(loadout) : null;
}
@ -119,38 +126,22 @@ public sealed class ScenarioStage
return entity;
}
// 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)
// Items in the entity's first cargo bay, one per name. A name is an equippable or consumable design (open: the stage
// chooses its product) or a product of one (pinned); a name that is neither is a failure.
public void Cargo(Entity entity, params string[] names)
{
if (entity == null) return;
var where = $"cargo of {entity.Name}";
var bay = entity.CargoBays.FirstOrDefault();
foreach (var design in designs)
foreach (var name in names)
{
var data = Named<CraftedItemData>(design, where, found => found is EquippableItemData || found is ConsumableItemData);
var (data, product) = NamedOrProduct<CraftedItemData>(name, where, found => found is EquippableItemData || found is ConsumableItemData);
if (data == null) continue;
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;
}
var build = Loadouts.Resolve(_items, _items.ItemData.RefOf(data),
product == null ? default : _items.ItemData.RefOf(product), _choose, where, _failures);
if (build == null) continue;
if (bay == null || !bay.TryStore(build()))
_failures.Add($"{where}: no room for {design}");
_failures.Add($"{where}: no room for {name}");
}
}
@ -163,7 +154,7 @@ public sealed class ScenarioStage
else
{
var failures = new List<string>();
entity = Loadouts.Materialize(_items, Arena, fit.Loadout, _available, failures);
entity = Loadouts.Materialize(_items, Arena, fit.Loadout, _choose, failures);
_failures.AddRange(failures.Select(failure => $"{where} ({fit.Loadout.Name}): {failure}"));
if (entity == null) return null;
}
@ -173,6 +164,17 @@ public sealed class ScenarioStage
return entity;
}
// A name is a design, or else exactly one product (names are unique) whose design is a T passing among: the design
// and that product. Neither is Named's failure.
private (T design, FactionProductData product) NamedOrProduct<T>(string name, string where, Func<T, bool> among = null) where T : CraftedItemData
{
var cache = _items.ItemData;
var products = cache.GetAll<FactionProductData>().Where(p => p.Name == name && cache.Get(p.Design) is T design && (among == null || among(design))).Take(2).ToArray();
if (products.Length == 1 && !cache.GetAll<T>().Any(data => data.Name == name && (among == null || among(data))))
return ((T) cache.Get(products[0].Design), products[0]);
return (Named(name, where, among), null);
}
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 && (among == null || among(data))).Take(2).ToArray();

View file

@ -26,8 +26,8 @@ public sealed class AiFusedDiscipline : Scenario
(weapon, int2(1, 8), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
("not if i see you first", int2(3, 10), None), // Compact Sensor
("Panopticon Prime", int2(1, 6), Clockwise)); // Fire Control Array
stage.Player(stage.Generated("Djinni"), float2(0, 0));
stage.Place(Armed("Smoke Refused"), Hostile, facing: float2(0, -1), stance: ScenarioStance.Hostile, piloted: true);

View file

@ -30,25 +30,25 @@ public sealed class Arcs : Scenario
("Rugged Drive", int2(4, 14), CounterClockwise),
("Rugged Drive", int2(5, 14), Clockwise),
("Compact Reactor", int2(4, 4), None),
("Guided Missile Launcher", int2(2, 5), None),
("Guided Missile Launcher", int2(7, 5), None),
("Cell Blaster", int2(3, 8), None),
("Cell Blaster", int2(6, 8), None),
("GT 3K", int2(2, 5), None), // Guided Missile Launcher
("GT 3K", int2(7, 5), None), // Guided Missile Launcher
("FastBlast+-", int2(3, 8), None), // Cell Blaster
("FastBlast+-", int2(6, 8), None), // Cell Blaster
("Radiator", int2(4, 2), CounterClockwise),
("Radiator", int2(5, 2), Clockwise),
("Compact Sensor", int2(5, 10), None),
("Fire Control Array", int2(3, 6), Clockwise),
("Autocannon", int2(8, 4), Clockwise),
("Charged Scatter Blaster", int2(0, 4), CounterClockwise));
("not if i see you first", int2(5, 10), None), // Compact Sensor
("Panopticon Prime", int2(3, 6), Clockwise), // Fire Control Array
("🫳🔫", int2(8, 4), Clockwise), // Autocannon
("ChargeBlast SG", int2(0, 4), CounterClockwise)); // Charged Scatter Blaster
var turret = stage.Fit("Turret",
("Light Machine Gun", int2(1, 2), None),
("Light Machine Gun", int2(5, 2), None),
("ClearPath", int2(1, 2), None), // Light Machine Gun
("ClearPath", int2(5, 2), None), // Light Machine Gun
("Long Radiator", int2(0, 2), CounterClockwise),
("Long Radiator", int2(7, 2), Clockwise),
("Compact Reactor", int2(3, 3), None),
("Turret Control Module", int2(3, 5), None),
("Compact Sensor", int2(4, 5), None),
("Fire Control Array", int2(3, 6), None));
("not if i see you first", int2(4, 5), None), // Compact Sensor
("Panopticon Prime", int2(3, 6), None)); // Fire Control Array
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));
stage.Place(stage.Bare("Longinus"), Bow);

View file

@ -26,14 +26,14 @@ public sealed class Belt : Scenario
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Lightning Projector", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("GT 3K", int2(0, 5), None), // Guided Missile Launcher
("GT 3K", int2(5, 5), None), // Guided Missile Launcher
("DragOnBreath", int2(1, 8), None), // Lightning Projector
("FastBlast+-", int2(4, 8), None), // Cell Blaster
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
("not if i see you first", int2(3, 10), None), // Compact Sensor
("Panopticon Prime", int2(1, 6), Clockwise)); // Fire Control Array
// The belt is a ring about its parent's position at the belt orbit's distance. The player stands 100 m outside
// it, at the point nearest the origin, facing along the ring.

View file

@ -36,14 +36,14 @@ public sealed class Crossing : Scenario
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Lightning Projector", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("GT 3K", int2(0, 5), None), // Guided Missile Launcher
("GT 3K", int2(5, 5), None), // Guided Missile Launcher
("DragOnBreath", int2(1, 8), None), // Lightning Projector
("FastBlast+-", int2(4, 8), None), // Cell Blaster
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
("not if i see you first", int2(3, 10), None), // Compact Sensor
("Panopticon Prime", int2(1, 6), Clockwise)); // Fire Control Array
stage.Player(fighter, Origin, facing: float2(0, 1));
stage.Place(stage.Generated("Djinni"), CrosserAt, facing: CrosserFacing, stance: ScenarioStance.Hostile, velocity: CrosserVelocity);

View file

@ -20,19 +20,19 @@ public sealed class Duel : Scenario
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("GT 3K", int2(0, 5), None), // Guided Missile Launcher
("GT 3K", int2(5, 5), None), // Guided Missile Launcher
("FastBlast+-", int2(1, 8), None), // Cell Blaster
("FastBlast+-", int2(4, 8), None), // Cell Blaster
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("not if i see you first", int2(3, 10), None), // Compact Sensor
("Capacitor", int2(1, 5), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Cargo Bay", int2(2, 8), None));
("Panopticon Prime", int2(1, 6), Clockwise), // Fire Control Array
("Store-All Plus", int2(2, 8), None)); // Cargo Bay
stage.Player(fighter, float2(0, 0));
var rival = stage.Place(fighter, float2(0, 900), facing: float2(0, -1), stance: ScenarioStance.Hostile, piloted: true);
stage.Cargo(rival, "Light Laser");
stage.Cargo(rival, "Spectra");
}
}

View file

@ -35,11 +35,11 @@ public sealed class FusedRounds : Scenario
("Compact Reactor", int2(4, 4), None),
("Smoke Proximity", int2(3, 8), None),
("Smoke Delayed", int2(6, 8), None),
("Cluster Cannon", int2(5, 17), None),
("DeathCluster", int2(5, 17), None), // Cluster Cannon
("Radiator", int2(4, 2), CounterClockwise),
("Radiator", int2(5, 2), Clockwise),
("Compact Sensor", int2(5, 10), None),
("Fire Control Array", int2(3, 6), Clockwise));
("not if i see you first", int2(5, 10), None), // Compact Sensor
("Panopticon Prime", int2(3, 6), Clockwise)); // Fire Control Array
stage.Player(shooter, float2(0, 0), facing: float2(0, 1));
stage.Place(stage.Bare("Longinus"), Mid, stance: ScenarioStance.Hostile);

View file

@ -20,14 +20,14 @@ public sealed class LauncherAngles : Scenario
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Swarm Rocket Pod", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("GT 3K", int2(0, 5), None), // Guided Missile Launcher
("Leonid", int2(5, 5), None), // Swarm Rocket Pod
("FastBlast+-", int2(1, 8), None), // Cell Blaster
("FastBlast+-", int2(4, 8), None), // Cell Blaster
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
("not if i see you first", int2(3, 10), None), // Compact Sensor
("Panopticon Prime", int2(1, 6), Clockwise)); // Fire Control Array
stage.Player(launcher, float2(0, 0));
stage.Place(stage.Bare("Longinus"), float2(0, 900), facing: float2(1, 0), stance: ScenarioStance.Hostile);

View file

@ -29,16 +29,16 @@ public sealed class LockOn : Scenario
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("GT 3K", int2(0, 5), None), // Guided Missile Launcher
("GT 3K", int2(5, 5), None), // Guided Missile Launcher
("FastBlast+-", int2(1, 8), None), // Cell Blaster
("FastBlast+-", int2(4, 8), None), // Cell Blaster
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("not if i see you first", int2(3, 10), None), // Compact Sensor
("Capacitor", int2(1, 5), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Cargo Bay", int2(2, 8), None));
("Panopticon Prime", int2(1, 6), Clockwise), // Fire Control Array
("Store-All Plus", int2(2, 8), None)); // Cargo Bay
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));
stage.Place(fighter, HostileAt, facing: normalize(-HostileAt), stance: ScenarioStance.Hostile, piloted: true);

View file

@ -26,15 +26,15 @@ public sealed class Pursuit : Scenario
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Mine Launcher", int2(0, 5), None),
("Swarm Rocket Pod", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("parting gift", int2(0, 5), None), // Mine Launcher
("Leonid", int2(5, 5), None), // Swarm Rocket Pod
("FastBlast+-", int2(1, 8), None), // Cell Blaster
("FastBlast+-", int2(4, 8), None), // Cell Blaster
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Cargo Bay", int2(2, 8), None));
("not if i see you first", int2(3, 10), None), // Compact Sensor
("Panopticon Prime", int2(1, 6), Clockwise), // Fire Control Array
("Store-All Plus", int2(2, 8), None)); // Cargo Bay
var pursuer = stage.Fit("Longinus",
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
@ -42,19 +42,19 @@ public sealed class Pursuit : Scenario
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("GT 3K", int2(0, 5), None), // Guided Missile Launcher
("GT 3K", int2(5, 5), None), // Guided Missile Launcher
("FastBlast+-", int2(1, 8), None), // Cell Blaster
("FastBlast+-", int2(4, 8), None), // Cell Blaster
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("not if i see you first", int2(3, 10), None), // Compact Sensor
("Capacitor", int2(1, 5), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Cargo Bay", int2(2, 8), None));
("Panopticon Prime", int2(1, 6), Clockwise), // Fire Control Array
("Store-All Plus", int2(2, 8), None)); // Cargo Bay
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.Cargo(player, "Morning Fog", "Morning Fog", "Morning Fog", "send it", "send it");
stage.Place(pursuer, float2(0, -Astern), facing: float2(0, 1), stance: ScenarioStance.Hostile, piloted: true);
}
}

View file

@ -28,11 +28,11 @@ public sealed class RefusedRounds : Scenario
("Smoke Refused", int2(3, 8), None),
("Smoke Refused Beam", int2(6, 8), None),
("Smoke Refused Charge", int2(3, 17), None),
("Autocannon", int2(5, 17), None),
("🫳🔫", int2(5, 17), None), // Autocannon
("Radiator", int2(4, 2), CounterClockwise),
("Radiator", int2(5, 2), Clockwise),
("Compact Sensor", int2(5, 10), None),
("Fire Control Array", int2(3, 6), Clockwise));
("not if i see you first", int2(5, 10), None), // Compact Sensor
("Panopticon Prime", int2(3, 6), Clockwise)); // Fire Control Array
stage.Player(shooter, float2(0, 0), facing: float2(0, 1));
stage.Place(stage.Bare("Longinus"), Target);

View file

@ -20,12 +20,12 @@ public sealed class StarvedReactor : Scenario
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Muon Fusion Reactor", int2(2, 4), None),
("Light Laser", int2(1, 8), None),
("Light Laser", int2(4, 8), None),
("Spectra", int2(1, 8), None), // Light Laser
("Spectra", int2(4, 8), None), // Light Laser
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
("not if i see you first", int2(3, 10), None), // Compact Sensor
("Panopticon Prime", int2(1, 6), Clockwise)); // Fire Control Array
stage.Player(starved, float2(0, 0));
stage.Place(stage.Bare("Longinus"), float2(0, 400));
}

View file

@ -24,13 +24,13 @@ public sealed class WeaponFeel : Scenario
("Rugged Drive", int2(4, 14), CounterClockwise),
("Rugged Drive", int2(5, 14), Clockwise),
("Compact Reactor", int2(4, 4), None),
("Charged Scatter Blaster", int2(3, 17), None),
("Autocannon", int2(5, 17), None),
("Cell Blaster", int2(3, 8), None),
("ChargeBlast SG", int2(3, 17), None), // Charged Scatter Blaster
("🫳🔫", int2(5, 17), None), // Autocannon
("FastBlast+-", int2(3, 8), None), // Cell Blaster
("Radiator", int2(4, 2), CounterClockwise),
("Radiator", int2(5, 2), Clockwise),
("Compact Sensor", int2(5, 10), None),
("Fire Control Array", int2(3, 6), Clockwise));
("not if i see you first", int2(5, 10), None), // Compact Sensor
("Panopticon Prime", int2(3, 6), Clockwise)); // Fire Control Array
stage.Player(shooter, float2(0, 0), facing: float2(0, 1));
stage.Place(stage.Bare("Longinus"), Target);