using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using GameCult.Aetheria.State.Unity; using Unity.Mathematics; using static Unity.Mathematics.math; using Random = Unity.Mathematics.Random; public class LoadoutGenerator { public Random Random; public ItemManager ItemManager { get; } public AetheriaRuntimeCatalogSnapshot RuntimeCatalog { get; } public Galaxy Galaxy { get; } public GalaxyZone Zone { get; } public Faction Faction { get; } public float PriceExponent { get; } public LoadoutGenerator( ref Random random, ItemManager itemManager, AetheriaRuntimeCatalogSnapshot runtimeCatalog, Galaxy galaxy, GalaxyZone zone, Faction faction, float priceExponent) { Random = random; ItemManager = itemManager; RuntimeCatalog = runtimeCatalog ?? throw new InvalidOperationException("Loadout generation requires the typed Aetheria runtime catalog."); Galaxy = galaxy; Zone = zone; Faction = faction; PriceExponent = priceExponent; } // // public LoadoutGenerator( // ref Random random, // ItemManager itemManager, // Faction faction, // float priceExponent) // { // Random = random; // ItemManager = itemManager; // Faction = faction; // PriceExponent = priceExponent; // } public RuntimeEntityBlueprint GenerateShipLoadout(Predicate hullFilter = null) { var 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; if(hull==null) ItemManager.Log("WHAT???"); var entity = new Ship(ItemManager, null, hull, ItemManager.GameplaySettings.DefaultEntitySettings); entity.Faction = Faction; OutfitEntity(entity); return RuntimeEntityBlueprintProjector.CaptureBlueprint(entity); } public RuntimeOrbitalEntityBlueprint GenerateTurretLoadout() { var 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 entity = new OrbitalEntity(ItemManager, null, hull, Guid.Empty, ItemManager.GameplaySettings.DefaultEntitySettings); entity.Faction = Faction; OutfitEntity(entity); return RuntimeEntityBlueprintProjector.CaptureBlueprint(entity) as RuntimeOrbitalEntityBlueprint; } public RuntimeOrbitalEntityBlueprint GenerateStationLoadout() { var 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 entity = new OrbitalEntity(ItemManager, null, hull, Guid.Empty, ItemManager.GameplaySettings.DefaultEntitySettings); entity.Faction = Faction; var emptyShape = entity.UnoccupiedSpace; var dockingBayData = RandomItem(2, item => item.Shape.FitsWithin(emptyShape, out _, out _), item => FitsWithin(item, emptyShape)); 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; dockingBay.Rotation = cargoRotation; if (!entity.TryEquip(dockingBay, cargoPosition)) { throw new InvalidLoadoutException("Failed to equip selected docking bay!"); } OutfitEntity(entity); var cargo = entity.CargoBays.First(); IEnumerable inventory = RandomItems(16, 1, data => !(data is HullData hull && hull.HullType != HullType.Ship) && !(data is CargoBayData), item => item.Category != "CargoBayData" && item.Category != "DockingBayData" && (item.Category != "HullData" || item.HullType == nameof(HullType.Ship))); inventory = inventory .OrderByDescending(item=>item.Shape.Coordinates.Length); foreach (var item in inventory) { var instance = ItemManager.CreateInstance(item); cargo.TryStore(instance); } entity.CanTow = hullData.CanTow; return RuntimeEntityBlueprintProjector.CaptureBlueprint(entity) as RuntimeOrbitalEntityBlueprint; } public HullData RandomHull(HullType type, Predicate hullFilter = null) { return RandomItem(0, item => (hullFilter?.Invoke(item) ?? true) && item.HullType == type, item => string.Equals(item.HullType, type.ToString(), StringComparison.Ordinal)); } public T[] RandomItems( int count, float sizeExponent, Predicate filter = null, Predicate typedFilter = null) where T : EquippableItemData { return RuntimeCatalog.EquipmentItems .Where(IsTypedCandidate) .Where(item => item.Price > 0 && Guid.TryParse(item.ManufacturerLegacyId, out var manufacturer) && manufacturer != Guid.Empty && (Galaxy.IsPrelude || Galaxy.ContainsFaction(manufacturer) && (Faction == null || Faction.Allegiance.ContainsKey(manufacturer))) && (typedFilter?.Invoke(item) ?? true)) .Select(ProjectRuntimeItem) .Where(item => item != null) .Where(item => item.Price > 0 && item.Manufacturer != Guid.Empty && (Galaxy.IsPrelude || Galaxy.ContainsFaction(item.Manufacturer) && (Faction == null || Faction.Allegiance.ContainsKey(item.Manufacturer))) && (filter?.Invoke(item) ?? true)) .WeightedRandomElements(ref Random, item => Faction == null ? 1 : (item.Manufacturer == Faction.ID ? 1 : Faction.Allegiance.ContainsKey(item.Manufacturer) ? Faction.Allegiance[item.Manufacturer] : 0.0f / // Prioritize items from allied manufacturers ManufacturerDistancePenalty(item.Manufacturer)) * // Penalize distance to manufacturer headquarters pow(item.Shape.Coordinates.Length, sizeExponent) / // Prioritize larger items pow(item.Price, PriceExponent), // Penalize item price to a controllable degree count ); } private static bool IsTypedCandidate(AetheriaRuntimeCatalogItem item) where T : EquippableItemData { var requestedType = typeof(T); if (requestedType == typeof(EquippableItemData)) { return !string.IsNullOrWhiteSpace(item.HardpointType); } if (requestedType == typeof(GearData)) { return item.Category == "GearData" || item.Category == "WeaponItemData"; } if (requestedType == typeof(CargoBayData)) { return item.Category == "CargoBayData" || item.Category == "DockingBayData"; } return string.Equals(item.Category, requestedType.Name, StringComparison.Ordinal); } private T ProjectRuntimeItem(AetheriaRuntimeCatalogItem item) where T : EquippableItemData { return Guid.TryParse(item.LegacyId, out var legacyId) ? ItemManager.GetRuntimeItemProjection(legacyId) : null; } private float ManufacturerDistancePenalty(Guid manufacturer) { if (Galaxy == null || Zone == null || !Galaxy.ContainsFaction(manufacturer)) { return 1; } var faction = Galaxy.ResolveFaction(manufacturer); return faction != null && Galaxy.HomeZones.TryGetValue(faction, out var homeZone) ? Zone.Distance[homeZone] : 1; } public T RandomItem( float sizeExponent, Predicate filter = null, Predicate typedFilter = null) where T : EquippableItemData { return RandomItems(1, sizeExponent, filter, typedFilter).FirstOrDefault(); } public T RandomItem(HardpointData hardpoint, float sizeExponent, Predicate filter = null) where T : EquippableItemData { return RandomItem( 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, item => FitsHardpoint(item, hardpoint)); } private static bool FitsHardpoint(AetheriaRuntimeCatalogItem item, HardpointData hardpoint) { return string.Equals(item.HardpointType, hardpoint.Type.ToString(), StringComparison.Ordinal) && item.OccupiedCells == hardpoint.Shape.Coordinates.Length && FitsWithin(item, hardpoint.Shape, hardpoint.Rotation); } private static bool HasBehaviorKind(AetheriaRuntimeCatalogItem item, string behaviorKind) { return !string.IsNullOrWhiteSpace(behaviorKind) && item.BehaviorKinds.Contains(behaviorKind, StringComparer.Ordinal); } private static bool FitsWithin(AetheriaRuntimeCatalogItem item, Shape target) { return ToShape(item.ShapeWidth, item.ShapeHeight, item.ShapeCells).FitsWithin(target, out _, out _); } private static bool FitsWithin(AetheriaRuntimeCatalogItem item, Shape target, ItemRotation rotation) { return ToShape(item.ShapeWidth, item.ShapeHeight, item.ShapeCells).FitsWithin(target, rotation, out _); } private static Shape ToShape(int width, int height, IReadOnlyList cells) { var shape = new Shape(Math.Max(width, 1), Math.Max(height, 1)); foreach (var cell in cells) { shape[int2(cell.X, cell.Y)] = true; } return shape; } private void OutfitEntity(Entity entity) { 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(); foreach (var hardpoint in hullData.Hardpoints.OrderByDescending(h=>h.Shape.Coordinates.Length)) { if (hardpoint.Type == HardpointType.ControlModule) { var controllerBehaviorKind = entity is Ship ? nameof(CockpitData) : entity is OrbitalEntity ? nameof(TurretControllerData) : null; var controllerData = RandomItem(hardpoint, 2, item => item.Behaviors.Any(b => entity is Ship && b is CockpitData || entity is OrbitalEntity && b is TurretControllerData), item => HasBehaviorKind(item, controllerBehaviorKind)); if (controllerData == null) throw new InvalidLoadoutException("No compatible controller found for entity!"); var controller = ItemManager.CreateInstance(controllerData) as EquippableItem; if (!entity.TryEquip(controller)) { throw new InvalidLoadoutException($"Failed to equip selected {Enum.GetName(typeof(HardpointType), hardpoint.Type)}!"); } } 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(hardpoint, 2); if (itemData == 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; if (!entity.TryEquip(item)) { throw new InvalidLoadoutException($"Failed to equip selected {Enum.GetName(typeof(HardpointType), hardpoint.Type)}!"); } previousItems.Add(itemData); } } } var emptyShape = entity.UnoccupiedSpace; var cargoData = RandomItem(3, item => !(item is DockingBayData) && item.Shape.FitsWithin(emptyShape, out _, out _), item => item.Category != "DockingBayData" && FitsWithin(item, emptyShape)); 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; cargo.Rotation = cargoRotation; if (!entity.TryEquip(cargo, cargoPosition)) throw new InvalidLoadoutException("Failed to equip selected cargo bay!"); emptyShape = entity.UnoccupiedSpace; var capacitorData = RandomItem(2, item => item.Behaviors.Any(b => b is CapacitorData) && item.Shape.FitsWithin(emptyShape, out _, out _), item => item.BehaviorKinds.Contains(nameof(CapacitorData), StringComparer.Ordinal) && FitsWithin(item, emptyShape)); 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; 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) { } }