2021-03-18 00:36:34 +00:00
using System ;
2021-03-18 22:48:33 +00:00
using System.Collections.Generic ;
2026-08-10 14:57:50 +01:00
using System.Diagnostics ;
2021-03-18 00:36:34 +00:00
using System.Linq ;
2026-08-10 14:57:50 +01:00
using Unity.Mathematics ;
using static Unity . Mathematics . math ;
using Random = Unity . Mathematics . Random ;
2021-03-18 00:36:34 +00:00
public class LoadoutGenerator
{
public Random Random ;
public ItemManager ItemManager { get ; }
2021-05-14 03:03:28 +01:00
public Galaxy Galaxy { get ; }
public GalaxyZone Zone { get ; }
2021-03-20 04:20:17 +00:00
public Faction Faction { get ; }
2021-03-18 00:36:34 +00:00
public float PriceExponent { get ; }
public LoadoutGenerator (
ref Random random ,
ItemManager itemManager ,
2021-05-14 03:03:28 +01:00
Galaxy galaxy ,
GalaxyZone zone ,
2021-03-20 04:20:17 +00:00
Faction faction ,
2021-03-18 00:36:34 +00:00
float priceExponent )
{
Random = random ;
ItemManager = itemManager ;
2021-05-14 03:03:28 +01:00
Galaxy = galaxy ;
2021-03-18 00:36:34 +00:00
Zone = zone ;
Faction = faction ;
PriceExponent = priceExponent ;
}
2026-08-10 14:57:50 +01:00
public EntityPack GenerateShipLoadout ( Predicate < HullData > hullFilter = null )
2021-03-18 00:36:34 +00:00
{
2026-08-10 14:57:50 +01:00
var hullData = RandomHull ( HullType . Ship , hullFilter ) ;
if ( hullData = = null )
2022-07-12 14:22:23 +01:00
{
ItemManager . Log ( "Unable to generate ship loadout: no compatible hull found!" ) ;
return null ;
}
2026-08-10 14:57:50 +01:00
var hull = ItemManager . CreateInstance ( hullData ) as EquippableItem ;
2021-04-30 11:19:25 +01:00
if ( hull = = null )
ItemManager . Log ( "WHAT???" ) ;
2021-03-18 00:36:34 +00:00
var entity = new Ship ( ItemManager , null , hull , ItemManager . GameplaySettings . DefaultEntitySettings ) ;
entity . Faction = Faction ;
OutfitEntity ( entity ) ;
2026-08-10 14:57:50 +01:00
return EntitySerializer . Pack ( entity ) ;
2021-03-18 00:36:34 +00:00
}
2026-08-10 14:57:50 +01:00
public OrbitalEntityPack GenerateTurretLoadout ( )
2021-03-18 00:36:34 +00:00
{
2026-08-10 14:57:50 +01:00
var hullData = RandomHull ( HullType . Turret ) ;
if ( hullData = = null )
2022-07-12 14:22:23 +01:00
{
ItemManager . Log ( "Unable to generate turret loadout: no compatible hull found!" ) ;
return null ;
}
2026-08-10 14:57:50 +01:00
var hull = ItemManager . CreateInstance ( hullData ) as EquippableItem ;
var entity = new OrbitalEntity ( ItemManager , null , hull , Guid . Empty , ItemManager . GameplaySettings . DefaultEntitySettings ) ;
2021-03-18 00:36:34 +00:00
entity . Faction = Faction ;
OutfitEntity ( entity ) ;
2026-08-10 14:57:50 +01:00
return EntitySerializer . Pack ( entity ) as OrbitalEntityPack ;
2021-03-18 00:36:34 +00:00
}
2026-08-10 14:57:50 +01:00
public OrbitalEntityPack GenerateStationLoadout ( )
2021-03-20 04:20:17 +00:00
{
2026-08-10 14:57:50 +01:00
var hullData = RandomHull ( HullType . Station ) ;
if ( hullData = = null )
2022-07-12 14:22:23 +01:00
{
ItemManager . Log ( "Unable to generate station loadout: no compatible hull found!" ) ;
return null ;
}
2026-08-10 14:57:50 +01:00
var hull = ItemManager . CreateInstance ( hullData ) as EquippableItem ;
var entity = new OrbitalEntity ( ItemManager , null , hull , Guid . Empty , ItemManager . GameplaySettings . DefaultEntitySettings ) ;
2021-03-20 04:20:17 +00:00
entity . Faction = Faction ;
2026-09-11 23:23:19 +02:00
// Hardpoint gear goes in first so the docking bay can only take space that gear left free
EquipHardpoints ( entity ) ;
2021-03-20 04:20:17 +00:00
var emptyShape = entity . UnoccupiedSpace ;
2026-09-11 23:23:19 +02:00
var dockingBayData = RandomItem < DockingBayData > ( 2 , item = > item . Shape . FitsWithin ( emptyShape , out _ , out _ ) , required : true ) ;
2026-08-10 14:57:50 +01:00
if ( dockingBayData = = null ) throw new InvalidLoadoutException ( "No compatible docking bay found for station!" ) ;
2021-03-20 04:20:17 +00:00
2026-08-10 14:57:50 +01:00
dockingBayData . Shape . FitsWithin ( emptyShape , out var cargoRotation , out var cargoPosition ) ;
var dockingBay = ItemManager . CreateInstance ( dockingBayData ) as EquippableItem ;
2021-03-20 04:20:17 +00:00
dockingBay . Rotation = cargoRotation ;
if ( ! entity . TryEquip ( dockingBay , cargoPosition ) )
{
throw new InvalidLoadoutException ( "Failed to equip selected docking bay!" ) ;
}
2026-09-11 23:23:19 +02:00
FillInterior ( entity ) ;
2021-03-20 04:20:17 +00:00
var cargo = entity . CargoBays . First ( ) ;
2026-08-10 14:57:50 +01:00
IEnumerable < EquippableItemData > inventory = RandomItems < EquippableItemData > ( 16 , 1 ,
data = > ! ( data is HullData hull & & hull . HullType ! = HullType . Ship ) & & ! ( data is CargoBayData ) ) ;
2026-06-14 05:25:56 +01:00
inventory = inventory
2026-08-10 14:57:50 +01:00
. OrderByDescending ( item = > item . Shape . Coordinates . Length ) ;
2021-03-20 04:20:17 +00:00
foreach ( var item in inventory )
{
2026-08-10 14:57:50 +01:00
var instance = ItemManager . CreateInstance ( item ) ;
2021-03-20 04:20:17 +00:00
cargo . TryStore ( instance ) ;
}
2021-06-18 12:32:03 +01:00
2026-08-10 14:57:50 +01:00
entity . CanTow = hullData . CanTow ;
2021-03-20 04:20:17 +00:00
2026-08-10 14:57:50 +01:00
return EntitySerializer . Pack ( entity ) as OrbitalEntityPack ;
2021-03-20 04:20:17 +00:00
}
2026-09-11 23:23:19 +02:00
// Every entity needs a hull, so hulls are always required
2026-08-10 14:57:50 +01:00
public HullData RandomHull ( HullType type , Predicate < HullData > hullFilter = null )
2021-03-18 00:36:34 +00:00
{
2026-09-11 23:23:19 +02:00
return RandomItem < HullData > ( 0 , item = >
2026-08-10 14:57:50 +01:00
( hullFilter ? . Invoke ( item ) ? ? true ) & &
2026-09-11 23:23:19 +02:00
item . HullType = = type , required : true ) ;
2021-03-18 00:36:34 +00:00
}
2026-09-11 23:23:19 +02:00
// 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
2021-03-18 00:36:34 +00:00
{
2026-09-11 23:23:19 +02:00
var candidates = ItemManager . ItemData . GetAll < T > ( )
. Where ( item = >
2026-06-13 13:35:00 +01:00
item . Price > 0 & &
2026-08-10 14:57:50 +01:00
item . Manufacturer ! = Guid . Empty & &
( filter ? . Invoke ( item ) ? ? true ) )
2026-09-11 23:23:19 +02:00
. ToArray ( ) ;
var available = candidates . Where ( IsAvailable ) . 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." ) ;
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
count ) ;
2021-03-20 04:20:17 +00:00
}
2026-09-11 23:23:19 +02:00
private bool IsAvailable ( EquippableItemData item ) = >
Galaxy . IsPrelude | |
Galaxy . ContainsFaction ( item . Manufacturer ) & & ( Faction = = null | | Faction . Allegiance . ContainsKey ( item . Manufacturer ) ) ;
// Prioritize items from the zone faction and its allies, penalizing distance to the manufacturer's headquarters
private float ManufacturerPreference ( Guid manufacturer )
2021-03-20 04:20:17 +00:00
{
2026-09-11 23:23:19 +02:00
if ( Faction = = null ) return 1 ;
var allegiance = manufacturer = = Faction . ID ? 1 :
Faction . Allegiance . TryGetValue ( manufacturer , out var a ) ? a : 0 ;
var home = Galaxy . HomeZones . FirstOrDefault ( h = > h . Key . ID = = manufacturer ) . Value ;
var distance = home ! = null & & Zone ? . Distance ! = null & & Zone . Distance . TryGetValue ( home , out var d ) ? d : 0 ;
return allegiance / ( 1 + distance ) ;
2021-03-18 00:36:34 +00:00
}
2026-09-11 23:23:19 +02:00
public T RandomItem < T > ( float sizeExponent , Predicate < T > filter = null , bool required = false ) where T : EquippableItemData
{
return RandomItems ( 1 , sizeExponent , filter , required ) . FirstOrDefault ( ) ;
}
public T RandomItem < T > ( HardpointData hardpoint , float sizeExponent , Predicate < T > filter = null , bool required = false ) where T : EquippableItemData
2026-06-13 23:20:16 +01:00
{
2026-08-10 14:57:50 +01:00
return RandomItem < T > ( sizeExponent , item = > item . HardpointType = = hardpoint . Type & &
2026-09-11 23:23:19 +02:00
( filter ? . Invoke ( item ) ? ? true ) & &
2026-08-10 14:57:50 +01:00
item . Shape . FitsWithin ( hardpoint . Shape , hardpoint . Rotation , out _ ) & &
2026-09-11 23:23:19 +02:00
item . Shape . Coordinates . Length = = hardpoint . Shape . Coordinates . Length , required ) ;
2026-06-14 05:25:56 +01:00
}
2021-03-18 00:36:34 +00:00
private void OutfitEntity ( Entity entity )
2026-09-11 23:23:19 +02:00
{
EquipHardpoints ( entity ) ;
FillInterior ( entity ) ;
}
private void EquipHardpoints ( Entity entity )
2021-03-18 00:36:34 +00:00
{
2026-08-10 14:57:50 +01:00
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 > ( ) ;
foreach ( var hardpoint in hullData . Hardpoints . OrderByDescending ( h = > h . Shape . Coordinates . Length ) )
2021-03-18 00:36:34 +00:00
{
2026-08-10 14:57:50 +01:00
if ( hardpoint . Type = = HardpointType . ControlModule )
2021-03-18 00:36:34 +00:00
{
2026-08-10 14:57:50 +01:00
var controllerData = RandomItem < GearData > ( hardpoint , 2 ,
2026-09-11 23:23:19 +02:00
item = > item . Behaviors . Any ( b = > entity is Ship & & b is CockpitData | | entity is OrbitalEntity & & b is TurretControllerData ) , required : true ) ;
2026-08-10 14:57:50 +01:00
if ( controllerData = = null )
2021-03-27 10:03:24 +00:00
throw new InvalidLoadoutException ( "No compatible controller found for entity!" ) ;
2026-08-10 14:57:50 +01:00
var controller = ItemManager . CreateInstance ( controllerData ) as EquippableItem ;
2021-03-18 00:36:34 +00:00
if ( ! entity . TryEquip ( controller ) )
{
2026-08-10 14:57:50 +01:00
throw new InvalidLoadoutException ( $"Failed to equip selected {Enum.GetName(typeof(HardpointType), hardpoint.Type)}!" ) ;
2021-03-18 00:36:34 +00:00
}
}
else
{
2021-03-20 04:20:17 +00:00
// If a previously selected item fits, use that one (this is why we must process larger hardpoints first)
2026-08-10 14:57:50 +01:00
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!" ) ;
2021-03-18 00:36:34 +00:00
else
{
2026-08-10 14:57:50 +01:00
//throw new InvalidLoadoutException($"No compatible item found for entity {Enum.GetName(typeof(HardpointType), hardpoint.Type)} hardpoint!");
2021-03-27 10:03:24 +00:00
EquippableItem item ;
if ( previousItem ! = null )
2026-08-10 14:57:50 +01:00
item = ItemManager . CreateInstance ( itemData , previousItem . EquippableItem . Quality ) as EquippableItem ;
else item = ItemManager . CreateInstance ( itemData ) as EquippableItem ;
2021-03-18 00:36:34 +00:00
if ( ! entity . TryEquip ( item ) )
{
2026-08-10 14:57:50 +01:00
throw new InvalidLoadoutException ( $"Failed to equip selected {Enum.GetName(typeof(HardpointType), hardpoint.Type)}!" ) ;
2021-03-18 00:36:34 +00:00
}
2026-08-10 14:57:50 +01:00
previousItems . Add ( itemData ) ;
2021-03-18 00:36:34 +00:00
}
}
}
2026-09-11 23:23:19 +02:00
}
// Interior gear may use any free interior cell, including empty hardpoint cells
private void FillInterior ( Entity entity )
{
2021-03-20 04:20:17 +00:00
var emptyShape = entity . UnoccupiedSpace ;
2026-09-11 23:23:19 +02:00
2026-08-10 14:57:50 +01:00
var cargoData = RandomItem < CargoBayData > ( 3 , item = >
! ( item is DockingBayData ) & &
2026-09-11 23:23:19 +02:00
item . Shape . FitsWithin ( emptyShape , out _ , out _ ) , required : true ) ;
2026-08-10 14:57:50 +01:00
if ( cargoData = = null ) throw new InvalidLoadoutException ( "No compatible cargo bay found for entity!" ) ;
2021-03-20 04:20:17 +00:00
2026-08-10 14:57:50 +01:00
cargoData . Shape . FitsWithin ( emptyShape , out var cargoRotation , out var cargoPosition ) ;
var cargo = ItemManager . CreateInstance ( cargoData ) as EquippableItem ;
2021-03-20 04:20:17 +00:00
cargo . Rotation = cargoRotation ;
if ( ! entity . TryEquip ( cargo , cargoPosition ) )
throw new InvalidLoadoutException ( "Failed to equip selected cargo bay!" ) ;
emptyShape = entity . UnoccupiedSpace ;
2021-03-18 00:36:34 +00:00
2026-08-10 14:57:50 +01:00
var capacitorData = RandomItem < GearData > ( 2 , item = >
item . Behaviors . Any ( b = > b is CapacitorData ) & &
2026-09-11 23:23:19 +02:00
item . Shape . FitsWithin ( emptyShape , out _ , out _ ) , required : true ) ;
2026-08-10 14:57:50 +01:00
if ( capacitorData = = null ) throw new InvalidLoadoutException ( "No compatible capacitor found for entity!" ) ;
2021-03-18 00:36:34 +00:00
2026-08-10 14:57:50 +01:00
capacitorData . Shape . FitsWithin ( emptyShape , out var capacitorRotation , out var capacitorPosition ) ;
var capacitor = ItemManager . CreateInstance ( capacitorData ) as EquippableItem ;
2021-03-18 00:36:34 +00:00
capacitor . Rotation = capacitorRotation ;
if ( ! entity . TryEquip ( capacitor , capacitorPosition ) )
throw new InvalidLoadoutException ( "Failed to equip selected capacitor!" ) ;
}
}
public class InvalidLoadoutException : Exception
{
public InvalidLoadoutException ( )
{
}
public InvalidLoadoutException ( string message )
: base ( message )
{
}
public InvalidLoadoutException ( string message , Exception inner )
: base ( message , inner )
{
}
2026-08-10 14:57:50 +01:00
}