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:
parent
95932da895
commit
8d93b937bc
11 changed files with 275 additions and 95 deletions
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue