namespace SpaceGame.Api.Industry.Planning; internal static class CommodityOperationalSignal { internal static float ComputeNeedScore(FactionCommoditySnapshot commodity, float targetLevelSeconds) { var productionDeficit = MathF.Max(0f, commodity.ConsumptionRatePerSecond - commodity.ProjectedProductionRatePerSecond); var levelDeficit = MathF.Max(0f, targetLevelSeconds - commodity.LevelSeconds) / MathF.Max(targetLevelSeconds, 1f); var backlogPressure = MathF.Max(0f, commodity.BuyBacklog + commodity.ReservedForConstruction - commodity.AvailableStock); var levelWeight = commodity.Level switch { CommodityLevelKind.Critical => 140f, CommodityLevelKind.Low => 80f, CommodityLevelKind.Stable => 20f, _ => 0f, }; return levelWeight + (productionDeficit * 140f) + (levelDeficit * 120f) + backlogPressure; } internal static bool IsOperational(FactionCommoditySnapshot commodity, float targetLevelSeconds) => commodity.ProjectedProductionRatePerSecond > 0.01f && commodity.ProjectedNetRatePerSecond >= -0.01f && commodity.LevelSeconds >= targetLevelSeconds && commodity.Level is CommodityLevelKind.Stable or CommodityLevelKind.Surplus; internal static bool IsStrained(FactionCommoditySnapshot commodity, float targetLevelSeconds) => !IsOperational(commodity, targetLevelSeconds) || commodity.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low; internal static float ComputeFeasibilityFactor(FactionCommoditySnapshot commodity, float targetLevelSeconds) { if (commodity.AvailableStock <= 0.01f && commodity.ProjectedProductionRatePerSecond <= 0.01f) { return 0.65f; } if (commodity.Level is CommodityLevelKind.Critical) { return 0.72f; } if (commodity.Level is CommodityLevelKind.Low || commodity.LevelSeconds < targetLevelSeconds) { return 0.84f; } return 1f; } }