Adds an `.editorconfig` file with C# and project-specific conventions. Applies consistent indentation and formatting across backend handlers, runtime models, and AI services.
206 lines
7.4 KiB
C#
206 lines
7.4 KiB
C#
namespace SpaceGame.Api.Industry.Planning;
|
|
|
|
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|
|
|
internal sealed class FactionEconomySnapshot
|
|
{
|
|
private readonly Dictionary<string, FactionCommoditySnapshot> commodities = new(StringComparer.Ordinal);
|
|
|
|
internal IReadOnlyDictionary<string, FactionCommoditySnapshot> 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;
|
|
}
|
|
}
|
|
}
|