Author manufacturer variation as faction products

A design lists role names; a faction product is a manufacturer's branded
build of one, carrying its name, its flavor text, and a pseudo-gaussian
quality distribution per role whose mean is the technology put in and
whose deviation is their quality control. A PerformanceStat may name the
role whose quality it reads, unset reading the item own workmanship as
before, so old saves behave unchanged. Generation selects products rather
than designs, which makes what a faction makes the availability rule.

This is Njordr shape at one dimension: role names, per-role derivation
and per-product distributions carry over when quality widens into
dimensions. No parts exist yet; the product authors the role quality.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-12 01:12:12 +02:00
commit 8d93b937bc
11 changed files with 275 additions and 95 deletions

View file

@ -16,11 +16,11 @@ public class LoadoutGenerator
public float PriceExponent { get; }
public LoadoutGenerator(
ref Random random,
ItemManager itemManager,
Galaxy galaxy,
GalaxyZone zone,
Faction faction,
ref Random random,
ItemManager itemManager,
Galaxy galaxy,
GalaxyZone zone,
Faction faction,
float priceExponent)
{
Random = random;
@ -30,16 +30,16 @@ public class LoadoutGenerator
Faction = faction;
PriceExponent = priceExponent;
}
public EntityPack GenerateShipLoadout(Predicate<HullData> hullFilter = null)
{
var hullData = RandomHull(HullType.Ship, hullFilter);
var (hullProduct, hullData) = RandomHull(HullType.Ship, hullFilter);
if(hullData==null)
{
ItemManager.Log("Unable to generate ship loadout: no compatible hull found!");
return null;
}
var hull = ItemManager.CreateInstance(hullData) as EquippableItem;
var hull = ItemManager.CreateInstance(hullProduct) as EquippableItem;
if(hull==null)
ItemManager.Log("WHAT???");
var entity = new Ship(ItemManager, null, hull, ItemManager.GameplaySettings.DefaultEntitySettings);
@ -50,13 +50,13 @@ public class LoadoutGenerator
public OrbitalEntityPack GenerateTurretLoadout()
{
var hullData = RandomHull(HullType.Turret);
var (hullProduct, hullData) = RandomHull(HullType.Turret);
if(hullData==null)
{
ItemManager.Log("Unable to generate turret loadout: no compatible hull found!");
return null;
}
var hull = ItemManager.CreateInstance(hullData) as EquippableItem;
var hull = ItemManager.CreateInstance(hullProduct) as EquippableItem;
var entity = new OrbitalEntity(ItemManager, null, hull, Guid.Empty, ItemManager.GameplaySettings.DefaultEntitySettings);
entity.Faction = Faction;
OutfitEntity(entity);
@ -65,13 +65,13 @@ public class LoadoutGenerator
public OrbitalEntityPack GenerateStationLoadout()
{
var hullData = RandomHull(HullType.Station);
var (hullProduct, hullData) = RandomHull(HullType.Station);
if(hullData==null)
{
ItemManager.Log("Unable to generate station loadout: no compatible hull found!");
return null;
}
var hull = ItemManager.CreateInstance(hullData) as EquippableItem;
var hull = ItemManager.CreateInstance(hullProduct) as EquippableItem;
var entity = new OrbitalEntity(ItemManager, null, hull, Guid.Empty, ItemManager.GameplaySettings.DefaultEntitySettings);
entity.Faction = Faction;
@ -80,11 +80,11 @@ public class LoadoutGenerator
var emptyShape = entity.UnoccupiedSpace;
var dockingBayData = RandomItem<DockingBayData>(2, item => item.Shape.FitsWithin(emptyShape, out _, out _), required: true);
var (dockingBayProduct, dockingBayData) = RandomProduct<DockingBayData>(2, item => item.Shape.FitsWithin(emptyShape, out _, out _), required: true);
if (dockingBayData == null) throw new InvalidLoadoutException("No compatible docking bay found for station!");
dockingBayData.Shape.FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition);
var dockingBay = ItemManager.CreateInstance(dockingBayData) as EquippableItem;
var dockingBay = ItemManager.CreateInstance(dockingBayProduct) as EquippableItem;
dockingBay.Rotation = cargoRotation;
if (!entity.TryEquip(dockingBay, cargoPosition))
{
@ -94,61 +94,63 @@ public class LoadoutGenerator
FillInterior(entity);
var cargo = entity.CargoBays.First();
IEnumerable<EquippableItemData> inventory = RandomItems<EquippableItemData>(16, 1,
data => !(data is HullData hull && hull.HullType != HullType.Ship) && !(data is CargoBayData));
inventory = inventory
.OrderByDescending(item=>item.Shape.Coordinates.Length);
foreach (var item in inventory)
var inventory = RandomProducts<EquippableItemData>(16, 1,
data => !(data is HullData hull && hull.HullType != HullType.Ship) && !(data is CargoBayData))
.OrderByDescending(entry => entry.design.Shape.Coordinates.Length);
foreach (var entry in inventory)
{
var instance = ItemManager.CreateInstance(item);
var instance = ItemManager.CreateInstance(entry.product);
cargo.TryStore(instance);
}
entity.CanTow = hullData.CanTow;
return EntitySerializer.Pack(entity) as OrbitalEntityPack;
}
// Every entity needs a hull, so hulls are always required
public HullData RandomHull(HullType type, Predicate<HullData> hullFilter = null)
public (FactionProductData product, HullData design) RandomHull(HullType type, Predicate<HullData> hullFilter = null)
{
return RandomItem<HullData>(0, item =>
return RandomProduct<HullData>(0, item =>
(hullFilter?.Invoke(item) ?? true) &&
item.HullType == type, required: true);
}
// Items normally come only from manufacturers present in the galaxy and known to the zone's faction.
// A required item falls back to any manufacturer when none of those make one; that means the item
// database lacks variety, so it is logged rather than hidden.
public T[] RandomItems<T>(int count, float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
// What a faction makes is which products it has, so generation selects branded products rather than designs.
// Products normally come only from manufacturers present in the galaxy and known to the zone's faction; a
// required item falls back to any manufacturer when none of those make one, which means the product table
// lacks variety, so it is logged rather than hidden.
public (FactionProductData product, T design)[] RandomProducts<T>(int count, float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
{
var candidates = ItemManager.ItemData.GetAll<T>()
.Where(item =>
item.Price > 0 &&
item.Manufacturer != Guid.Empty &&
(filter?.Invoke(item) ?? true))
var candidates = ItemManager.ItemData.GetAll<FactionProductData>()
.Select(product => (product, design: ItemManager.ItemData.Get<CraftedItemData>(product.Design) as T))
.Where(entry =>
entry.design != null &&
entry.design.Price > 0 &&
entry.product.Manufacturer != Guid.Empty &&
(filter?.Invoke(entry.design) ?? true))
.ToArray();
var available = candidates.Where(IsAvailable).ToArray();
var available = candidates.Where(entry => IsAvailable(entry.product)).ToArray();
var preferManufacturers = true;
if (available.Length == 0 && required && candidates.Length > 0)
{
ItemManager.Log($"No {typeof(T).Name} available from manufacturers known to {Faction?.Name ?? "this galaxy"}; using any manufacturer. The item database needs more variety.");
ItemManager.Log($"No {typeof(T).Name} product available from manufacturers known to {Faction?.Name ?? "this galaxy"}; using any manufacturer. The product table needs more variety.");
available = candidates;
preferManufacturers = false;
}
return available.WeightedRandomElements(ref Random, item =>
(preferManufacturers ? ManufacturerPreference(item.Manufacturer) : 1) *
pow(item.Shape.Coordinates.Length, sizeExponent) / // Prioritize larger items
pow(item.Price, PriceExponent), // Penalize item price to a controllable degree
return available.WeightedRandomElements(ref Random, entry =>
(preferManufacturers ? ManufacturerPreference(entry.product.Manufacturer) : 1) *
pow(entry.design.Shape.Coordinates.Length, sizeExponent) / // Prioritize larger items
pow(entry.design.Price, PriceExponent), // Penalize item price to a controllable degree
count);
}
private bool IsAvailable(EquippableItemData item) =>
private bool IsAvailable(FactionProductData product) =>
Galaxy.IsPrelude ||
Galaxy.ContainsFaction(item.Manufacturer) && (Faction == null || Faction.Allegiance.ContainsKey(item.Manufacturer));
Galaxy.ContainsFaction(product.Manufacturer) && (Faction == null || Faction.Allegiance.ContainsKey(product.Manufacturer));
// Prioritize items from the zone faction and its allies, penalizing distance to the manufacturer's headquarters
// Prioritize products from the zone faction and its allies, penalizing distance to the manufacturer's headquarters
private float ManufacturerPreference(Guid manufacturer)
{
if (Faction == null) return 1;
@ -159,14 +161,14 @@ public class LoadoutGenerator
return allegiance / (1 + distance);
}
public T RandomItem<T>(float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
public (FactionProductData product, T design) RandomProduct<T>(float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
{
return RandomItems(1, sizeExponent, filter, required).FirstOrDefault();
return RandomProducts(1, sizeExponent, filter, required).FirstOrDefault();
}
public T RandomItem<T>(HardpointData hardpoint, float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
public (FactionProductData product, T design) RandomProduct<T>(HardpointData hardpoint, float sizeExponent, Predicate<T> filter = null, bool required = false) where T : EquippableItemData
{
return RandomItem<T>(sizeExponent, item => item.HardpointType == hardpoint.Type &&
return RandomProduct<T>(sizeExponent, item => item.HardpointType == hardpoint.Type &&
(filter?.Invoke(item) ?? true) &&
item.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _) &&
item.Shape.Coordinates.Length==hardpoint.Shape.Coordinates.Length, required);
@ -182,16 +184,16 @@ public class LoadoutGenerator
{
var hullData = ItemManager.GetData(entity.Hull) as HullData;
foreach (var v in hullData.Shape.Coordinates) entity.HullConductivity[v.x, v.y] = true;
var previousItems = new List<EquippableItemData>();
var previousProducts = new List<(FactionProductData product, GearData design)>();
foreach (var hardpoint in hullData.Hardpoints.OrderByDescending(h=>h.Shape.Coordinates.Length))
{
if (hardpoint.Type == HardpointType.ControlModule)
{
var controllerData = RandomItem<GearData>(hardpoint, 2,
var (controllerProduct, controllerData) = RandomProduct<GearData>(hardpoint, 2,
item => item.Behaviors.Any(b => entity is Ship && b is CockpitData || entity is OrbitalEntity && b is TurretControllerData), required: true);
if (controllerData == null)
if (controllerData == null)
throw new InvalidLoadoutException("No compatible controller found for entity!");
var controller = ItemManager.CreateInstance(controllerData) as EquippableItem;
var controller = ItemManager.CreateInstance(controllerProduct) as EquippableItem;
if (!entity.TryEquip(controller))
{
throw new InvalidLoadoutException($"Failed to equip selected {Enum.GetName(typeof(HardpointType), hardpoint.Type)}!");
@ -199,24 +201,22 @@ public class LoadoutGenerator
}
else
{
// If a previously selected item fits, use that one (this is why we must process larger hardpoints first)
var itemData = previousItems
.FirstOrDefault(i => i.HardpointType == hardpoint.Type && i.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _));
var previousItem = entity.Equipment.FirstOrDefault(item => item.Data == itemData);
itemData ??= RandomItem<GearData>(hardpoint, 2);
if (itemData == null) ItemManager.Log($"No compatible item found for entity {Enum.GetName(typeof(HardpointType), hardpoint.Type)} hardpoint!");
// If a previously selected product fits, use that one (this is why we must process larger hardpoints first)
var entry = previousProducts
.FirstOrDefault(e => e.design.HardpointType == hardpoint.Type && e.design.Shape.FitsWithin(hardpoint.Shape, hardpoint.Rotation, out _));
var previousItem = entity.Equipment.FirstOrDefault(item => item.Data == entry.design);
if (entry.design == null) entry = RandomProduct<GearData>(hardpoint, 2);
if (entry.design == null) ItemManager.Log($"No compatible item found for entity {Enum.GetName(typeof(HardpointType), hardpoint.Type)} hardpoint!");
else
{
//throw new InvalidLoadoutException($"No compatible item found for entity {Enum.GetName(typeof(HardpointType), hardpoint.Type)} hardpoint!");
EquippableItem item;
if(previousItem!=null)
item = ItemManager.CreateInstance(itemData, previousItem.EquippableItem.Quality) as EquippableItem;
else item = ItemManager.CreateInstance(itemData) as EquippableItem;
var item = ItemManager.CreateInstance(entry.product) as EquippableItem;
// Matching hardpoints carry matching units: the same brand, workmanship, and parts
if (previousItem != null) CopyBuild(previousItem.EquippableItem, item);
if (!entity.TryEquip(item))
{
throw new InvalidLoadoutException($"Failed to equip selected {Enum.GetName(typeof(HardpointType), hardpoint.Type)}!");
}
previousItems.Add(itemData);
previousProducts.Add(entry);
}
}
}
@ -228,30 +228,39 @@ public class LoadoutGenerator
{
var emptyShape = entity.UnoccupiedSpace;
var cargoData = RandomItem<CargoBayData>(3, item =>
var (cargoProduct, cargoData) = RandomProduct<CargoBayData>(3, item =>
!(item is DockingBayData) &&
item.Shape.FitsWithin(emptyShape, out _, out _), required: true);
if (cargoData == null) throw new InvalidLoadoutException("No compatible cargo bay found for entity!");
cargoData.Shape.FitsWithin(emptyShape, out var cargoRotation, out var cargoPosition);
var cargo = ItemManager.CreateInstance(cargoData) as EquippableItem;
var cargo = ItemManager.CreateInstance(cargoProduct) as EquippableItem;
cargo.Rotation = cargoRotation;
if (!entity.TryEquip(cargo, cargoPosition))
throw new InvalidLoadoutException("Failed to equip selected cargo bay!");
emptyShape = entity.UnoccupiedSpace;
var capacitorData = RandomItem<GearData>(2, item =>
var (capacitorProduct, capacitorData) = RandomProduct<GearData>(2, item =>
item.Behaviors.Any(b => b is CapacitorData) &&
item.Shape.FitsWithin(emptyShape, out _, out _), required: true);
if (capacitorData == null) throw new InvalidLoadoutException("No compatible capacitor found for entity!");
capacitorData.Shape.FitsWithin(emptyShape, out var capacitorRotation, out var capacitorPosition);
var capacitor = ItemManager.CreateInstance(capacitorData) as EquippableItem;
var capacitor = ItemManager.CreateInstance(capacitorProduct) as EquippableItem;
capacitor.Rotation = capacitorRotation;
if (!entity.TryEquip(capacitor, capacitorPosition))
throw new InvalidLoadoutException("Failed to equip selected capacitor!");
}
// A second unit off the same line: same workmanship and the same parts, not a fresh roll.
private static void CopyBuild(CraftedItemInstance from, CraftedItemInstance to)
{
to.Quality = from.Quality;
to.Ingredients = from.Ingredients?
.Select(fill => new RoleFill { Role = fill.Role, Quality = fill.Quality })
.ToList() ?? new List<RoleFill>();
}
}
public class InvalidLoadoutException : Exception
@ -269,4 +278,4 @@ public class InvalidLoadoutException : Exception
: base(message, inner)
{
}
}
}