namespace SpaceGame.Api.Shared.Runtime; internal static class SimulationRuntimeSupport { internal static bool HasShipCapabilities(ShipDefinition definition, params string[] capabilities) => capabilities.All(cap => definition.Capabilities.Contains(cap, StringComparer.Ordinal)); internal static int CountStationModules(StationRuntime station, ModuleType moduleType) => station.Modules.Count(module => module.ModuleType == moduleType); internal static float GetStationSupportedPopulation( IReadOnlyDictionary moduleDefinitions, StationRuntime station) => 40f + station.Modules .Select(module => moduleDefinitions.TryGetValue(module.ModuleId, out var definition) && definition is HabitationModuleDefinition habitation ? habitation.SupportedPopulation : 0f) .Sum(); internal static float GetStationRequiredWorkforce( IReadOnlyDictionary moduleDefinitions, StationRuntime station) => MathF.Max(12f, station.Modules .Select(module => moduleDefinitions.TryGetValue(module.ModuleId, out var definition) && definition is ProductionLaneModuleDefinition productionLane ? productionLane.RequiredWorkforce : 0f) .Sum()); internal static float GetStationStorageCapacity(SimulationWorld world, StationRuntime station, StorageKind storageKind) { SyncStorageModuleLevels(world, station, storageKind); return GetStorageModules(world, station, storageKind) .Sum(entry => entry.Definition.StorageCapacity); } internal static bool HasStorageCapacity(SimulationWorld world, StationRuntime station, StorageKind storageKind) { SyncStorageModuleLevels(world, station, storageKind); return GetStorageModules(world, station, storageKind).Any(); } private static IEnumerable<(StorageStationModuleRuntime Module, StorageModuleDefinition Definition)> GetStorageModules( SimulationWorld world, StationRuntime station, StorageKind storageKind) => station.Modules .OfType() .Where(module => module.StorageKind == storageKind) .Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) && definition is StorageModuleDefinition storageDefinition ? (Module: module, Definition: storageDefinition) : ((StorageStationModuleRuntime Module, StorageModuleDefinition Definition)?)null) .Where(entry => entry is not null && entry.Value.Definition.StorageKind == storageKind) .Select(entry => entry!.Value); private static void SyncStorageModuleLevels(SimulationWorld world, StationRuntime station, StorageKind storageKind) { var storageModules = GetStorageModules(world, station, storageKind) .OrderBy(entry => entry.Module.Id, StringComparer.Ordinal) .ToList(); if (storageModules.Count == 0) { return; } var remaining = station.Inventory .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageKind) .Sum(entry => entry.Value); foreach (var (module, definition) in storageModules) { module.CurrentLevel = MathF.Min(remaining, definition.StorageCapacity); remaining = MathF.Max(0f, remaining - definition.StorageCapacity); } } internal static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId) { if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition)) { return; } station.Modules.Add(StationModuleRuntime.Create($"{station.Id}-module-{station.Modules.Count + 1}", definition)); station.Radius = GetStationRadius(world, station); } internal static float GetStationRadius(SimulationWorld world, StationRuntime station) { var totalArea = station.Modules .Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f) .Sum(); return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f))); } internal static int CountModules(IEnumerable modules, string moduleId) => modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal)); internal static float GetInventoryAmount(IReadOnlyDictionary inventory, string itemId) => inventory.TryGetValue(itemId, out var amount) ? amount : 0f; internal static void AddInventory(IDictionary inventory, string itemId, float amount) { if (amount <= 0f) { return; } inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary)inventory, itemId) + amount; } internal static float RemoveInventory(IDictionary inventory, string itemId, float amount) { var current = GetInventoryAmount((IReadOnlyDictionary)inventory, itemId); var removed = MathF.Min(current, amount); var remaining = current - removed; if (remaining <= 0.001f) { inventory.Remove(itemId); } else { inventory[itemId] = remaining; } return removed; } internal static bool HasStationModules(StationRuntime station, params string[] modules) => modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal))); internal static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node, SimulationWorld world) => HasShipCapabilities(ship.Definition, "mining") && world.ItemDefinitions.TryGetValue(node.ItemId, out var item) && item.CargoKind is not null && item.CargoKind == ship.Definition.CargoKind; internal static bool CanBuildClaimBeacon(ShipRuntime ship) => string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal); internal static float ComputeWorkforceRatio(float population, float workforceRequired) { if (workforceRequired <= 0.01f) { return 1f; } var staffedRatio = MathF.Min(1f, population / workforceRequired); return 0.1f + (0.9f * staffedRatio); } internal static string? GetStorageRequirement( IReadOnlyDictionary moduleDefinitions, StorageKind? storageKind) { if (storageKind is not { } requiredStorageKind) { return null; } return moduleDefinitions.Values .OfType() .Where(definition => definition.StorageKind == requiredStorageKind) .OrderBy(definition => GetPreferredStorageModuleRank(definition.Id)) .ThenBy(definition => definition.Id, StringComparer.Ordinal) .Select(definition => definition.Id) .FirstOrDefault(); } private static int GetPreferredStorageModuleRank(string moduleId) { if (moduleId.Contains("_m_", StringComparison.Ordinal)) { return 0; } if (moduleId.Contains("_s_", StringComparison.Ordinal)) { return 1; } if (moduleId.Contains("_l_", StringComparison.Ordinal)) { return 2; } return 3; } internal static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) { if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) { return 0f; } var storageKind = itemDefinition.CargoKind; if (storageKind is null) { return 0f; } if (!HasStorageCapacity(world, station, storageKind.Value)) { return 0f; } var capacity = GetStationStorageCapacity(world, station, storageKind.Value); if (capacity <= 0.01f) { return 0f; } var used = station.Inventory .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageKind) .Sum(entry => entry.Value); var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used)); if (accepted <= 0.01f) { return 0f; } AddInventory(station.Inventory, itemId, accepted); return accepted; } internal static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) => recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount); internal static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) => world.ConstructionSites.FirstOrDefault(site => string.Equals(site.StationId, stationId, StringComparison.Ordinal) && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed); internal static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId) { if (site.StationId is not null && world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station) { return GetInventoryAmount(station.Inventory, itemId); } return GetInventoryAmount(site.DeliveredItems, itemId); } internal static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) => site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value); internal static float GetShipCargoAmount(ShipRuntime ship) => ship.Inventory.Values.Sum(); }