Adds an `.editorconfig` file with C# and project-specific conventions. Applies consistent indentation and formatting across backend handlers, runtime models, and AI services.
591 lines
24 KiB
C#
591 lines
24 KiB
C#
namespace SpaceGame.Api.Industry.Planning;
|
|
|
|
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|
|
|
internal static class FactionIndustryPlanner
|
|
{
|
|
private const float CommodityTargetLevelSeconds = 240f;
|
|
private const float WaterTargetLevelSeconds = 300f;
|
|
|
|
internal static IndustryExpansionProject? AnalyzeCommodityNeed(SimulationWorld world, string factionId, string commodityId, bool ignoreActiveExpansionProject = false)
|
|
{
|
|
if (!ignoreActiveExpansionProject && HasActiveExpansionProject(world, factionId))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var bottleneckCommodity = ResolveBottleneckCommodity(world, factionId, commodityId);
|
|
var moduleId = world.ProductionGraph.GetPrimaryProducerModule(bottleneckCommodity);
|
|
if (moduleId is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var targetCelestial = SelectFoundationCelestial(world, factionId, bottleneckCommodity);
|
|
if (targetCelestial is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var supportStation = SelectSupportStation(world, factionId, moduleId, targetCelestial.SystemId);
|
|
if (supportStation is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new IndustryExpansionProject(
|
|
bottleneckCommodity,
|
|
moduleId,
|
|
targetCelestial.SystemId,
|
|
targetCelestial.Id,
|
|
supportStation.Id);
|
|
}
|
|
|
|
internal static IndustryExpansionProject? AnalyzeShipyardNeed(SimulationWorld world, string factionId, bool ignoreActiveExpansionProject = false)
|
|
{
|
|
if (!ignoreActiveExpansionProject && HasActiveExpansionProject(world, factionId))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
const string shipyardModuleId = "module_gen_build_l_01";
|
|
if (world.Stations.Any(station =>
|
|
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
|
&& station.InstalledModules.Contains(shipyardModuleId, StringComparer.Ordinal)))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!world.ModuleRecipes.TryGetValue(shipyardModuleId, out var shipyardRecipe))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var bottleneckCommodity = shipyardRecipe.Inputs
|
|
.Select(input => ResolveBottleneckCommodity(world, factionId, input.ItemId))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.Select(itemId => new
|
|
{
|
|
ItemId = itemId,
|
|
Commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId),
|
|
})
|
|
.Where(entry => entry.Commodity.ProjectedProductionRatePerSecond <= 0.01f
|
|
|| CommodityOperationalSignal.IsStrained(entry.Commodity, GetTargetLevelSeconds(entry.ItemId)))
|
|
.OrderByDescending(entry => entry.Commodity.ProjectedProductionRatePerSecond <= 0.01f ? 1 : 0)
|
|
.ThenByDescending(entry => CommodityOperationalSignal.ComputeNeedScore(entry.Commodity, GetTargetLevelSeconds(entry.ItemId)))
|
|
.ThenBy(entry => entry.Commodity.AvailableStock)
|
|
.Select(entry => entry.ItemId)
|
|
.FirstOrDefault();
|
|
|
|
if (!string.IsNullOrWhiteSpace(bottleneckCommodity))
|
|
{
|
|
return AnalyzeCommodityNeed(world, factionId, bottleneckCommodity, ignoreActiveExpansionProject);
|
|
}
|
|
|
|
return CreateShipyardFoundationProject(world, factionId, ignoreActiveExpansionProject);
|
|
}
|
|
|
|
internal static IndustryExpansionProject? CreateShipyardFoundationProject(SimulationWorld world, string factionId, bool ignoreActiveExpansionProject = false)
|
|
{
|
|
const string shipyardModuleId = "module_gen_build_l_01";
|
|
if (!ignoreActiveExpansionProject && HasActiveExpansionProject(world, factionId))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var targetCelestial = SelectLogisticsFoundationCelestial(world, factionId);
|
|
if (targetCelestial is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var supportStation = SelectSupportStation(world, factionId, shipyardModuleId, targetCelestial.SystemId);
|
|
if (supportStation is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new IndustryExpansionProject(
|
|
"shipyard",
|
|
shipyardModuleId,
|
|
targetCelestial.SystemId,
|
|
targetCelestial.Id,
|
|
supportStation.Id);
|
|
}
|
|
|
|
internal static IndustryExpansionProject? AnalyzeExpansionNeed(SimulationWorld world, string factionId)
|
|
{
|
|
if (HasActiveExpansionProject(world, factionId))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var bootstrapCommodity = SelectBootstrapCommodity(world, factionId);
|
|
if (bootstrapCommodity is not null)
|
|
{
|
|
var bootstrapModuleId = world.ProductionGraph.GetPrimaryProducerModule(bootstrapCommodity);
|
|
if (bootstrapModuleId is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var bootstrapCelestial = SelectFoundationCelestial(world, factionId, bootstrapCommodity);
|
|
if (bootstrapCelestial is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var bootstrapSupportStation = SelectSupportStation(world, factionId, bootstrapModuleId, bootstrapCelestial.SystemId);
|
|
if (bootstrapSupportStation is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new IndustryExpansionProject(
|
|
bootstrapCommodity,
|
|
bootstrapModuleId,
|
|
bootstrapCelestial.SystemId,
|
|
bootstrapCelestial.Id,
|
|
bootstrapSupportStation.Id);
|
|
}
|
|
|
|
var commodityId = SelectCommodityToExpand(world, factionId);
|
|
if (commodityId is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var moduleId = world.ProductionGraph.GetPrimaryProducerModule(commodityId);
|
|
if (moduleId is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var targetCelestial = SelectFoundationCelestial(world, factionId, commodityId);
|
|
if (targetCelestial is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var supportStation = SelectSupportStation(world, factionId, moduleId, targetCelestial.SystemId);
|
|
if (supportStation is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new IndustryExpansionProject(
|
|
commodityId,
|
|
moduleId,
|
|
targetCelestial.SystemId,
|
|
targetCelestial.Id,
|
|
supportStation.Id);
|
|
}
|
|
|
|
internal static IndustryExpansionProject? GetActiveExpansionProject(SimulationWorld world, string factionId)
|
|
{
|
|
var site = world.ConstructionSites.FirstOrDefault(candidate =>
|
|
string.Equals(candidate.FactionId, factionId, StringComparison.Ordinal)
|
|
&& string.Equals(candidate.TargetKind, "station-foundation", StringComparison.Ordinal)
|
|
&& candidate.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
|
|
if (site is null || site.BlueprintId is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var supportStationId = world.Stations
|
|
.Where(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal))
|
|
.OrderByDescending(station => station.SystemId == site.SystemId ? 1 : 0)
|
|
.ThenByDescending(station => station.Inventory.Values.Sum())
|
|
.Select(station => station.Id)
|
|
.FirstOrDefault();
|
|
if (supportStationId is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new IndustryExpansionProject(
|
|
site.TargetDefinitionId,
|
|
site.BlueprintId,
|
|
site.SystemId,
|
|
site.CelestialId,
|
|
supportStationId,
|
|
site.Id);
|
|
}
|
|
|
|
internal static void EnsureExpansionSite(SimulationWorld world, string factionId, IndustryExpansionProject project)
|
|
{
|
|
if (project.SiteId is not null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanEstablishExpansionSite(world, factionId, project))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var nowUtc = DateTimeOffset.UtcNow;
|
|
var claimId = $"claim-{factionId}-{project.CelestialId}";
|
|
if (world.Claims.All(candidate => candidate.Id != claimId))
|
|
{
|
|
world.Claims.Add(new ClaimRuntime
|
|
{
|
|
Id = claimId,
|
|
FactionId = factionId,
|
|
SystemId = project.SystemId,
|
|
CelestialId = project.CelestialId,
|
|
PlacedAtUtc = nowUtc,
|
|
ActivatesAtUtc = nowUtc.AddSeconds(8),
|
|
State = ClaimStateKinds.Activating,
|
|
Health = 100f,
|
|
});
|
|
}
|
|
|
|
if (!world.ModuleRecipes.TryGetValue(project.ModuleId, out var recipe))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var siteId = $"site-{factionId}-{project.CelestialId}";
|
|
if (world.ConstructionSites.Any(candidate => candidate.Id == siteId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var site = new ConstructionSiteRuntime
|
|
{
|
|
Id = siteId,
|
|
FactionId = factionId,
|
|
SystemId = project.SystemId,
|
|
CelestialId = project.CelestialId,
|
|
TargetKind = "station-foundation",
|
|
TargetDefinitionId = project.CommodityId,
|
|
BlueprintId = project.ModuleId,
|
|
ClaimId = claimId,
|
|
StationId = null,
|
|
State = ConstructionSiteStateKinds.Planned,
|
|
};
|
|
|
|
foreach (var input in recipe.Inputs)
|
|
{
|
|
site.RequiredItems[input.ItemId] = input.Amount;
|
|
site.DeliveredItems[input.ItemId] = 0f;
|
|
var orderId = $"market-order-{site.Id}-{input.ItemId}";
|
|
site.MarketOrderIds.Add(orderId);
|
|
world.MarketOrders.Add(new MarketOrderRuntime
|
|
{
|
|
Id = orderId,
|
|
FactionId = factionId,
|
|
StationId = project.SupportStationId,
|
|
ConstructionSiteId = site.Id,
|
|
Kind = MarketOrderKinds.Buy,
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
RemainingAmount = input.Amount,
|
|
Valuation = 1.1f,
|
|
State = MarketOrderStateKinds.Open,
|
|
});
|
|
}
|
|
|
|
if (world.Stations.FirstOrDefault(station => station.Id == project.SupportStationId) is { } supportStation)
|
|
{
|
|
foreach (var orderId in site.MarketOrderIds)
|
|
{
|
|
supportStation.MarketOrderIds.Add(orderId);
|
|
}
|
|
}
|
|
|
|
world.ConstructionSites.Add(site);
|
|
}
|
|
|
|
private static string? SelectCommodityToExpand(SimulationWorld world, string factionId)
|
|
{
|
|
var demandByItem = world.MarketOrders
|
|
.Where(order =>
|
|
string.Equals(order.FactionId, factionId, StringComparison.Ordinal)
|
|
&& string.Equals(order.Kind, MarketOrderKinds.Buy, StringComparison.Ordinal)
|
|
&& order.RemainingAmount > 0.01f)
|
|
.GroupBy(order => order.ItemId, StringComparer.Ordinal)
|
|
.ToDictionary(group => group.Key, group => group.Sum(order => order.RemainingAmount), StringComparer.Ordinal);
|
|
|
|
var threatAssessment = CommanderPlanningService.FindFactionThreatAssessment(world, factionId);
|
|
if (threatAssessment is not null && threatAssessment.EnemyFactionCount > 0)
|
|
{
|
|
demandByItem["hullparts"] = demandByItem.GetValueOrDefault("hullparts") + 120f;
|
|
demandByItem["claytronics"] = demandByItem.GetValueOrDefault("claytronics") + 90f;
|
|
}
|
|
|
|
return demandByItem
|
|
.Select(entry =>
|
|
{
|
|
var itemId = ResolveBottleneckCommodity(world, factionId, entry.Key);
|
|
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId);
|
|
var score = entry.Value + CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(itemId));
|
|
return (ItemId: itemId, Score: score);
|
|
})
|
|
.Where(entry => entry.ItemId is not null)
|
|
.GroupBy(entry => entry.ItemId!, StringComparer.Ordinal)
|
|
.Select(group => (ItemId: group.Key, Score: group.Sum(entry => entry.Score)))
|
|
.OrderByDescending(entry => entry.Score)
|
|
.Select(entry => entry.ItemId)
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
private static string? SelectBootstrapCommodity(SimulationWorld world, string factionId)
|
|
{
|
|
if (!FactionHasProducerForCommodity(world, factionId, "refinedmetals"))
|
|
{
|
|
return "refinedmetals";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string ResolveBottleneckCommodity(SimulationWorld world, string factionId, string itemId)
|
|
{
|
|
var visited = new HashSet<string>(StringComparer.Ordinal);
|
|
return ResolveBottleneckCommodity(world, factionId, itemId, visited);
|
|
}
|
|
|
|
private static string ResolveBottleneckCommodity(SimulationWorld world, string factionId, string itemId, HashSet<string> visited)
|
|
{
|
|
if (!visited.Add(itemId))
|
|
{
|
|
return itemId;
|
|
}
|
|
|
|
var producers = world.ProductionGraph.GetProcessesForOutput(itemId);
|
|
if (producers.Count == 0)
|
|
{
|
|
return itemId;
|
|
}
|
|
|
|
var hasFactionProducer = producers
|
|
.SelectMany(process => process.RequiredModuleIds)
|
|
.Any(moduleId => world.Stations.Any(station =>
|
|
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
|
&& station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)));
|
|
if (!hasFactionProducer)
|
|
{
|
|
return itemId;
|
|
}
|
|
|
|
var weakestUnproducedInput = world.ProductionGraph.GetImmediateInputs(itemId)
|
|
.Where(inputId => !FactionHasProducerForCommodity(world, factionId, inputId))
|
|
.Select(inputId =>
|
|
{
|
|
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(inputId);
|
|
return (ItemId: inputId, Score: CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(inputId)), Stockpile: commodity.AvailableStock);
|
|
})
|
|
.OrderByDescending(entry => entry.Score)
|
|
.ThenBy(entry => entry.Stockpile)
|
|
.FirstOrDefault();
|
|
|
|
if (!string.IsNullOrWhiteSpace(weakestUnproducedInput.ItemId)
|
|
&& (weakestUnproducedInput.Score > 0.01f || weakestUnproducedInput.Stockpile < 120f))
|
|
{
|
|
return ResolveBottleneckCommodity(world, factionId, weakestUnproducedInput.ItemId, visited);
|
|
}
|
|
|
|
var weakestInput = world.ProductionGraph.GetImmediateInputs(itemId)
|
|
.Select(inputId =>
|
|
{
|
|
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(inputId);
|
|
return (ItemId: inputId, Score: CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(inputId)));
|
|
})
|
|
.OrderByDescending(entry => entry.Score)
|
|
.FirstOrDefault();
|
|
|
|
return weakestInput.Score > GetCommodityNeedScore(world, factionId, itemId) * 0.6f
|
|
? ResolveBottleneckCommodity(world, factionId, weakestInput.ItemId, visited)
|
|
: itemId;
|
|
}
|
|
|
|
internal static bool FactionHasProducerForCommodity(SimulationWorld world, string factionId, string itemId)
|
|
=> FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId).ProjectedProductionRatePerSecond > 0.01f;
|
|
|
|
internal static IReadOnlyCollection<string> ResolveRootResourceItems(SimulationWorld world, string commodityId)
|
|
{
|
|
var frontier = new Queue<string>();
|
|
var resources = new HashSet<string>(StringComparer.Ordinal);
|
|
var visited = new HashSet<string>(StringComparer.Ordinal);
|
|
frontier.Enqueue(commodityId);
|
|
|
|
while (frontier.Count > 0)
|
|
{
|
|
var current = frontier.Dequeue();
|
|
if (!visited.Add(current))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var inputs = world.ProductionGraph.GetImmediateInputs(current);
|
|
if (inputs.Count == 0)
|
|
{
|
|
resources.Add(current);
|
|
continue;
|
|
}
|
|
|
|
foreach (var input in inputs)
|
|
{
|
|
frontier.Enqueue(input);
|
|
}
|
|
}
|
|
|
|
return resources.Count > 0 ? resources : [commodityId];
|
|
}
|
|
|
|
private static bool HasActiveExpansionProject(SimulationWorld world, string factionId) =>
|
|
world.ConstructionSites.Any(site =>
|
|
string.Equals(site.FactionId, factionId, StringComparison.Ordinal)
|
|
&& string.Equals(site.TargetKind, "station-foundation", StringComparison.Ordinal)
|
|
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
|
|
|
|
private static float GetCommodityNeedScore(SimulationWorld world, string factionId, string itemId)
|
|
{
|
|
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId);
|
|
return CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(itemId));
|
|
}
|
|
|
|
private static float GetTargetLevelSeconds(string itemId) =>
|
|
string.Equals(itemId, "water", StringComparison.Ordinal) ? WaterTargetLevelSeconds : CommodityTargetLevelSeconds;
|
|
|
|
private static CelestialRuntime? SelectFoundationCelestial(SimulationWorld world, string factionId, string commodityId)
|
|
{
|
|
var resourceItems = ResolveRootResourceItems(world, commodityId);
|
|
return world.Celestials
|
|
.Where(celestial =>
|
|
celestial.Kind == SpatialNodeKind.LagrangePoint
|
|
&& celestial.OccupyingStructureId is null
|
|
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed)
|
|
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId))
|
|
.OrderByDescending(celestial => ScoreCelestial(world, factionId, celestial, resourceItems))
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
private static CelestialRuntime? SelectLogisticsFoundationCelestial(SimulationWorld world, string factionId)
|
|
{
|
|
return world.Celestials
|
|
.Where(celestial =>
|
|
celestial.Kind == SpatialNodeKind.LagrangePoint
|
|
&& celestial.OccupyingStructureId is null
|
|
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed)
|
|
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId))
|
|
.OrderByDescending(celestial => world.Stations.Count(station =>
|
|
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
|
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal)))
|
|
.ThenByDescending(celestial => world.Stations
|
|
.Where(station =>
|
|
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
|
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal))
|
|
.Sum(station => station.Inventory.Values.Sum()))
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
private static float ScoreCelestial(SimulationWorld world, string factionId, CelestialRuntime celestial, IReadOnlyCollection<string> resourceItems)
|
|
{
|
|
var resourceScore = world.Nodes
|
|
.Where(node => node.SystemId == celestial.SystemId && resourceItems.Contains(node.ItemId, StringComparer.Ordinal))
|
|
.Sum(node => node.OreRemaining);
|
|
var factionPresence = world.Stations.Count(station =>
|
|
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
|
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal));
|
|
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, celestial.SystemId);
|
|
var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, celestial.SystemId);
|
|
var strategicProfile = world.Geopolitics?.Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == celestial.SystemId);
|
|
var pressure = world.Geopolitics?.Territory.Pressures
|
|
.Where(entry => entry.SystemId == celestial.SystemId && entry.FactionId == factionId)
|
|
.OrderByDescending(entry => entry.HostileInfluence)
|
|
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
|
|
.FirstOrDefault();
|
|
var controlBias = string.Equals(controlState?.ControllerFactionId, factionId, StringComparison.Ordinal)
|
|
? 12_000f
|
|
: string.Equals(controlState?.PrimaryClaimantFactionId, factionId, StringComparison.Ordinal)
|
|
? 4_000f
|
|
: 0f;
|
|
var regionBias = region is null
|
|
? 0f
|
|
: region.Kind switch
|
|
{
|
|
"core-industry" => 4_500f,
|
|
"shipbuilding" => 3_250f,
|
|
"trade-hub" => 2_250f,
|
|
"corridor" => 1_500f,
|
|
_ => 1_000f,
|
|
};
|
|
var securityPenalty = ((pressure?.HostileInfluence ?? 0f) * 14f)
|
|
+ ((strategicProfile?.TerritorialPressure ?? 0f) * 9f)
|
|
+ ((world.Geopolitics is null ? 0f : GeopoliticalSimulationService.GetSystemRouteRisk(world, celestial.SystemId, factionId)) * 250f);
|
|
return resourceScore
|
|
+ (factionPresence * 5_000f)
|
|
+ controlBias
|
|
+ regionBias
|
|
- securityPenalty;
|
|
}
|
|
|
|
private static bool IsExpansionSystemEligible(SimulationWorld world, string factionId, string systemId)
|
|
{
|
|
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, systemId);
|
|
if (controlState is null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var authorityFactionId = controlState.ControllerFactionId ?? controlState.PrimaryClaimantFactionId;
|
|
if (authorityFactionId is null || string.Equals(authorityFactionId, factionId, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!GeopoliticalSimulationService.HasMilitaryAccess(world, factionId, authorityFactionId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return !GeopoliticalSimulationService.HasHostileRelation(world, factionId, authorityFactionId);
|
|
}
|
|
|
|
private static bool CanEstablishExpansionSite(SimulationWorld world, string factionId, IndustryExpansionProject project)
|
|
{
|
|
if (!IsExpansionSystemEligible(world, factionId, project.SystemId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, project.SystemId);
|
|
if (controlState?.IsContested == true)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var pressure = world.Geopolitics?.Territory.Pressures
|
|
.Where(entry => entry.SystemId == project.SystemId && entry.FactionId == factionId)
|
|
.OrderByDescending(entry => entry.CorridorRisk + entry.HostileInfluence)
|
|
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
|
|
.FirstOrDefault();
|
|
return pressure is null || (pressure.CorridorRisk < 0.8f && pressure.HostileInfluence < 1.2f);
|
|
}
|
|
|
|
private static StationRuntime? SelectSupportStation(SimulationWorld world, string factionId, string moduleId, string targetSystemId)
|
|
{
|
|
var constructionInputs = world.ModuleRecipes.TryGetValue(moduleId, out var recipe)
|
|
? recipe.Inputs.Select(input => input.ItemId).ToList()
|
|
: [];
|
|
|
|
return world.Stations
|
|
.Where(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal))
|
|
.OrderByDescending(station => station.SystemId == targetSystemId ? 1 : 0)
|
|
.ThenByDescending(station => constructionInputs.Sum(inputId => GetInventoryAmount(station.Inventory, inputId)))
|
|
.ThenByDescending(station => station.Inventory.Values.Sum())
|
|
.FirstOrDefault();
|
|
}
|
|
}
|
|
|
|
internal sealed record IndustryExpansionProject(
|
|
string CommodityId,
|
|
string ModuleId,
|
|
string SystemId,
|
|
string CelestialId,
|
|
string SupportStationId,
|
|
string? SiteId = null);
|