namespace SpaceGame.Api.Industry.Planning; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; internal sealed class FactionEconomySnapshot { private readonly Dictionary commodities = new(StringComparer.Ordinal); internal IReadOnlyDictionary Commodities => commodities; internal FactionCommoditySnapshot GetCommodity(string itemId) { if (!commodities.TryGetValue(itemId, out var commodity)) { commodity = new FactionCommoditySnapshot(itemId); commodities[itemId] = commodity; } return commodity; } } internal sealed class FactionCommoditySnapshot { internal FactionCommoditySnapshot(string itemId) { ItemId = itemId; } internal string ItemId { get; } internal float OnHand { get; set; } internal float ReservedForConstruction { get; set; } internal float BuyBacklog { get; set; } internal float SellBacklog { get; set; } internal float Inbound { get; set; } internal float ProductionRatePerSecond { get; set; } internal float CommittedProductionRatePerSecond { get; set; } internal float ConsumptionRatePerSecond { get; set; } internal float AvailableStock => MathF.Max(0f, OnHand + Inbound - ReservedForConstruction); internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond; internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond; internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond; internal float OperationalUsageRatePerSecond => MathF.Max(ConsumptionRatePerSecond, BuyBacklog / 180f); internal float LevelSeconds => AvailableStock <= 0.01f ? 0f : AvailableStock / MathF.Max(OperationalUsageRatePerSecond, 0.01f); internal CommodityLevelKind Level => LevelSeconds switch { <= 60f => CommodityLevelKind.Critical, <= 180f => CommodityLevelKind.Low, <= 480f => CommodityLevelKind.Stable, _ => CommodityLevelKind.Surplus, }; } internal enum CommodityLevelKind { Critical, Low, Stable, Surplus, } internal static class FactionEconomyAnalyzer { internal static FactionEconomySnapshot Build(SimulationWorld world, string factionId) { var snapshot = new FactionEconomySnapshot(); foreach (var station in world.Stations.Where(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal))) { foreach (var (itemId, amount) in station.Inventory) { snapshot.GetCommodity(itemId).OnHand += amount; } foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station)) { var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey); if (recipe is null) { continue; } var throughput = StationSimulationService.GetStationProductionThroughput(world, station, recipe); var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f); if (cyclesPerSecond <= 0.0001f) { continue; } foreach (var input in recipe.Inputs) { snapshot.GetCommodity(input.ItemId).ConsumptionRatePerSecond += input.Amount * cyclesPerSecond; } foreach (var output in recipe.Outputs) { snapshot.GetCommodity(output.ItemId).ProductionRatePerSecond += output.Amount * cyclesPerSecond; } } } foreach (var order in world.MarketOrders.Where(order => string.Equals(order.FactionId, factionId, StringComparison.Ordinal) && order.State != MarketOrderStateKinds.Cancelled && order.RemainingAmount > 0.01f)) { var commodity = snapshot.GetCommodity(order.ItemId); if (string.Equals(order.Kind, MarketOrderKinds.Buy, StringComparison.Ordinal)) { commodity.BuyBacklog += order.RemainingAmount; } else if (string.Equals(order.Kind, MarketOrderKinds.Sell, StringComparison.Ordinal)) { commodity.SellBacklog += order.RemainingAmount; } } foreach (var site in world.ConstructionSites.Where(site => string.Equals(site.FactionId, factionId, StringComparison.Ordinal) && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed)) { ApplyCommittedProduction(world, snapshot, site); foreach (var required in site.RequiredItems) { var remaining = MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key)); if (remaining > 0.01f) { snapshot.GetCommodity(required.Key).ReservedForConstruction += remaining; } } } return snapshot; } private static void ApplyCommittedProduction( SimulationWorld world, FactionEconomySnapshot snapshot, ConstructionSiteRuntime site) { if (string.IsNullOrWhiteSpace(site.BlueprintId) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)) { return; } var recipeOutputs = world.Recipes.Values .Where(candidate => string.Equals(StationSimulationService.GetStationProductionLaneKey(world, candidate), site.BlueprintId, StringComparison.Ordinal)) .SelectMany(candidate => candidate.Outputs) .GroupBy(output => output.ItemId, StringComparer.Ordinal) .ToDictionary(group => group.Key, group => group.Sum(output => output.Amount), StringComparer.Ordinal); if (recipeOutputs.Count == 0) { return; } var materialFraction = 0f; var materialTerms = 0; foreach (var required in site.RequiredItems) { materialTerms += 1; materialFraction += required.Value <= 0.01f ? 1f : Math.Clamp(GetConstructionDeliveredAmount(world, site, required.Key) / required.Value, 0f, 1f); } materialFraction = materialTerms == 0 ? 1f : materialFraction / materialTerms; var buildFraction = recipe.Duration <= 0.01f ? 0f : Math.Clamp(site.Progress / recipe.Duration, 0f, 1f); var readiness = site.State switch { ConstructionSiteStateKinds.Active => 0.3f, ConstructionSiteStateKinds.Planned => 0.15f, _ => 0f, }; readiness += materialFraction * 0.45f; readiness += buildFraction * 0.25f; if (site.AssignedConstructorShipIds.Count > 0) { readiness += 0.1f; } readiness = Math.Clamp(readiness, 0f, 1f); if (readiness <= 0.01f) { return; } var cyclesPerSecond = readiness / MathF.Max(recipe.Duration, 0.01f); foreach (var (productItemId, amount) in recipeOutputs) { snapshot.GetCommodity(productItemId).CommittedProductionRatePerSecond += amount * cyclesPerSecond; } } }