Deepen faction economy and station planning
This commit is contained in:
@@ -105,16 +105,66 @@ internal sealed class InfrastructureSimulationService
|
||||
|
||||
internal static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world)
|
||||
{
|
||||
// Expand storage before it becomes a bottleneck
|
||||
const float StorageExpansionThreshold = 0.85f;
|
||||
var storageExpansionCandidates = new[]
|
||||
var economy = FactionEconomyAnalyzer.Build(world, station.FactionId);
|
||||
return GetModuleExpansionCandidates(world, station, economy)
|
||||
.Where(candidate => world.ModuleRecipes.ContainsKey(candidate.ModuleId))
|
||||
.OrderByDescending(candidate => candidate.Score)
|
||||
.Select(candidate => candidate.ModuleId)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ModuleExpansionCandidate> GetModuleExpansionCandidates(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
FactionEconomySnapshot economy)
|
||||
{
|
||||
var role = StationSimulationService.DetermineStationRole(station);
|
||||
var candidates = new Dictionary<string, float>(StringComparer.Ordinal);
|
||||
var constructionDemandByItem = GetOutstandingConstructionDemand(world, station.FactionId);
|
||||
var objectiveCommodity = GetObjectiveCommodityId(role);
|
||||
var objectiveModuleId = GetObjectiveModuleId(world, role, objectiveCommodity);
|
||||
|
||||
if (objectiveModuleId is not null && world.ModuleRecipes.TryGetValue(objectiveModuleId, out var objectiveRecipe))
|
||||
{
|
||||
AddOrRaiseCandidate(candidates, objectiveModuleId, ScoreObjectiveModule(world, station, economy, constructionDemandByItem, objectiveCommodity, objectiveModuleId));
|
||||
|
||||
foreach (var storageModuleId in GetRequiredStorageModules(world, objectiveRecipe))
|
||||
{
|
||||
if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal))
|
||||
{
|
||||
AddOrRaiseCandidate(candidates, storageModuleId, ScoreStorageModule(world, station, storageModuleId, objectiveModuleId, objectiveCommodity, requiredByObjective: true));
|
||||
}
|
||||
}
|
||||
|
||||
if (objectiveCommodity is not null
|
||||
&& world.ProductionGraph.GetImmediateInputs(objectiveCommodity).Contains("energycells", StringComparer.Ordinal))
|
||||
{
|
||||
AddOrRaiseCandidate(candidates, "module_gen_prod_energycells_01", ScoreEnergySupportModule(world, station, economy, constructionDemandByItem));
|
||||
}
|
||||
}
|
||||
|
||||
AddOrRaiseCandidate(candidates, "module_arg_dock_m_01_lowtech", ScoreDockModule(station));
|
||||
AddOrRaiseCandidate(candidates, "module_arg_hab_m_01", ScoreHabitationModule(station, world, economy));
|
||||
|
||||
foreach (var storageModuleId in GetStoragePressureCandidates(world, station))
|
||||
{
|
||||
AddOrRaiseCandidate(candidates, storageModuleId, ScoreStorageModule(world, station, storageModuleId, objectiveModuleId, objectiveCommodity, requiredByObjective: false));
|
||||
}
|
||||
|
||||
return candidates
|
||||
.Where(entry => entry.Value > 0.01f)
|
||||
.Select(entry => new ModuleExpansionCandidate(entry.Key, entry.Value))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetStoragePressureCandidates(SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
foreach (var (storageClass, moduleId) in new[]
|
||||
{
|
||||
("solid", "module_arg_stor_solid_m_01"),
|
||||
("liquid", "module_arg_stor_liquid_m_01"),
|
||||
("container", "module_arg_stor_container_m_01"),
|
||||
};
|
||||
|
||||
foreach (var (storageClass, moduleId) in storageExpansionCandidates)
|
||||
})
|
||||
{
|
||||
var capacity = GetStationStorageCapacity(station, storageClass);
|
||||
if (capacity <= 0.01f)
|
||||
@@ -123,51 +173,552 @@ internal sealed class InfrastructureSimulationService
|
||||
}
|
||||
|
||||
var used = station.Inventory
|
||||
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageClass)
|
||||
.Sum(entry => entry.Value);
|
||||
|
||||
if (used / capacity >= StorageExpansionThreshold && world.ModuleRecipes.ContainsKey(moduleId))
|
||||
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageClass)
|
||||
.Sum(entry => entry.Value);
|
||||
if (used / capacity >= 0.65f)
|
||||
{
|
||||
return moduleId;
|
||||
yield return moduleId;
|
||||
}
|
||||
}
|
||||
|
||||
var priorities = StationSimulationService.GetFactionExpansionPressure(world, station.FactionId) > 0f
|
||||
? new (string ModuleId, int TargetCount)[]
|
||||
{
|
||||
("module_gen_prod_refinedmetals_01", 1),
|
||||
("module_arg_stor_solid_m_01", 1),
|
||||
("module_arg_stor_container_m_01", 1),
|
||||
("module_gen_prod_hullparts_01", 2),
|
||||
("module_gen_prod_advancedelectronics_01", 1),
|
||||
("module_gen_build_l_01", 1),
|
||||
("module_arg_dock_m_01_lowtech", 2),
|
||||
("module_gen_prod_energycells_01", 2),
|
||||
}
|
||||
: new (string ModuleId, int TargetCount)[]
|
||||
{
|
||||
("module_gen_prod_refinedmetals_01", 1),
|
||||
("module_arg_stor_solid_m_01", 1),
|
||||
("module_arg_stor_container_m_01", 1),
|
||||
("module_gen_prod_hullparts_01", 2),
|
||||
("module_gen_prod_advancedelectronics_01", 1),
|
||||
("module_gen_build_l_01", 1),
|
||||
("module_gen_prod_energycells_01", 2),
|
||||
("module_arg_dock_m_01_lowtech", 2),
|
||||
};
|
||||
|
||||
foreach (var (moduleId, targetCount) in priorities)
|
||||
{
|
||||
if (CountModules(station.InstalledModules, moduleId) < targetCount
|
||||
&& world.ModuleRecipes.ContainsKey(moduleId))
|
||||
{
|
||||
return moduleId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetRequiredStorageModules(SimulationWorld world, ModuleRecipeDefinition recipe)
|
||||
{
|
||||
var itemIds = recipe.Inputs.Select(input => input.ItemId);
|
||||
foreach (var itemId in itemIds)
|
||||
{
|
||||
if (!world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetStorageRequirement(itemDefinition.CargoKind) is { } storageModuleId)
|
||||
{
|
||||
yield return storageModuleId;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return "module_arg_stor_container_m_01";
|
||||
}
|
||||
}
|
||||
|
||||
if (world.ModuleDefinitions.TryGetValue(recipe.ModuleId, out var moduleDefinition))
|
||||
{
|
||||
foreach (var productItemId in moduleDefinition.Products)
|
||||
{
|
||||
if (!world.ItemDefinitions.TryGetValue(productItemId, out var itemDefinition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GetStorageRequirement(itemDefinition.CargoKind) is { } storageModuleId)
|
||||
{
|
||||
yield return storageModuleId;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return "module_arg_stor_container_m_01";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetObjectiveCommodityId(string role) =>
|
||||
role switch
|
||||
{
|
||||
"power" => "energycells",
|
||||
"refinery" => "refinedmetals",
|
||||
"water" => "water",
|
||||
"hullparts" => "hullparts",
|
||||
"claytronics" => "claytronics",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static string? GetObjectiveModuleId(SimulationWorld world, string role, string? objectiveCommodityId) =>
|
||||
role switch
|
||||
{
|
||||
"shipyard" => "module_gen_build_l_01",
|
||||
_ => objectiveCommodityId is null ? null : world.ProductionGraph.GetPrimaryProducerModule(objectiveCommodityId),
|
||||
};
|
||||
|
||||
private static float ScoreObjectiveModule(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
FactionEconomySnapshot economy,
|
||||
IReadOnlyDictionary<string, float> constructionDemandByItem,
|
||||
string? objectiveCommodityId,
|
||||
string objectiveModuleId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(objectiveCommodityId))
|
||||
{
|
||||
var hasShipyard = CountModules(station.InstalledModules, objectiveModuleId);
|
||||
return hasShipyard == 0 ? 240f : 0f;
|
||||
}
|
||||
|
||||
var commodity = economy.GetCommodity(objectiveCommodityId);
|
||||
var currentCount = CountModules(station.InstalledModules, objectiveModuleId);
|
||||
var marginalOutputRate = EstimateMarginalOutputRate(world, station, objectiveModuleId, objectiveCommodityId);
|
||||
var constructionImpact = EstimateConstructionBottleneckImpact(world, objectiveModuleId, constructionDemandByItem);
|
||||
var score = 90f + commodity.ProjectedPressureScore + (marginalOutputRate * 900f) + constructionImpact;
|
||||
|
||||
if (currentCount == 0)
|
||||
{
|
||||
score += 80f;
|
||||
}
|
||||
|
||||
if (!float.IsPositiveInfinity(commodity.ProjectedShortageHorizonSeconds))
|
||||
{
|
||||
score += MathF.Max(0f, 300f - commodity.ProjectedShortageHorizonSeconds) * 0.3f;
|
||||
}
|
||||
|
||||
score *= EstimateObjectiveExpansionFeasibility(world, station, economy, objectiveModuleId, objectiveCommodityId);
|
||||
score *= EstimateProducerReadiness(world, station, economy, objectiveModuleId, objectiveCommodityId);
|
||||
score += EstimateImmediateProducerActivationScore(world, station, economy, objectiveModuleId, objectiveCommodityId);
|
||||
return score - (currentCount * 35f);
|
||||
}
|
||||
|
||||
private static float ScoreEnergySupportModule(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
FactionEconomySnapshot economy,
|
||||
IReadOnlyDictionary<string, float> constructionDemandByItem)
|
||||
{
|
||||
var energy = economy.GetCommodity("energycells");
|
||||
var currentCount = CountModules(station.InstalledModules, "module_gen_prod_energycells_01");
|
||||
var constructionImpact = EstimateConstructionBottleneckImpact(world, "module_gen_prod_energycells_01", constructionDemandByItem);
|
||||
var readinessUnlock = EstimateSupportUnlockScore(world, station, economy, "module_gen_prod_energycells_01");
|
||||
var score = 40f + energy.ProjectedPressureScore * 0.5f + constructionImpact + readinessUnlock;
|
||||
|
||||
if (currentCount == 0)
|
||||
{
|
||||
score += 70f;
|
||||
}
|
||||
|
||||
if (!float.IsPositiveInfinity(energy.ProjectedShortageHorizonSeconds))
|
||||
{
|
||||
score += MathF.Max(0f, 240f - energy.ProjectedShortageHorizonSeconds) * 0.2f;
|
||||
}
|
||||
|
||||
return score - (currentCount * 40f);
|
||||
}
|
||||
|
||||
private static float ScoreStorageModule(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
string storageModuleId,
|
||||
string? objectiveModuleId,
|
||||
string? objectiveCommodityId,
|
||||
bool requiredByObjective)
|
||||
{
|
||||
var storageClass = storageModuleId switch
|
||||
{
|
||||
"module_arg_stor_solid_m_01" => "solid",
|
||||
"module_arg_stor_liquid_m_01" => "liquid",
|
||||
_ => "container",
|
||||
};
|
||||
|
||||
var capacity = GetStationStorageCapacity(station, storageClass);
|
||||
var used = station.Inventory
|
||||
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageClass)
|
||||
.Sum(entry => entry.Value);
|
||||
var utilization = capacity <= 0.01f ? 0f : used / capacity;
|
||||
|
||||
var score = requiredByObjective ? 140f : 0f;
|
||||
score += MathF.Max(0f, utilization - 0.6f) * 240f;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(objectiveModuleId) && !string.IsNullOrWhiteSpace(objectiveCommodityId))
|
||||
{
|
||||
var objectiveUsesStorage = ModuleNeedsStorageClass(world, objectiveModuleId, storageClass)
|
||||
|| CommodityUsesStorageClass(world, objectiveCommodityId, storageClass);
|
||||
if (objectiveUsesStorage)
|
||||
{
|
||||
score += 35f;
|
||||
score += EstimateSupportUnlockScore(world, station, economy: null, supportModuleId: storageModuleId);
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static float ScoreDockModule(StationRuntime station)
|
||||
{
|
||||
var dockingPads = GetDockingPadCount(station);
|
||||
var dockedShips = station.DockedShipIds.Count;
|
||||
if (dockingPads <= 0)
|
||||
{
|
||||
return 150f;
|
||||
}
|
||||
|
||||
return dockedShips >= dockingPads ? 80f : dockingPads < 4 ? 25f : 0f;
|
||||
}
|
||||
|
||||
private static float ScoreHabitationModule(StationRuntime station)
|
||||
{
|
||||
if (station.WorkforceRequired <= 0.01f)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return station.WorkforceEffectiveRatio < 0.75f
|
||||
? 30f
|
||||
: station.WorkforceEffectiveRatio < 0.95f
|
||||
? 10f
|
||||
: 0f;
|
||||
}
|
||||
|
||||
private static float ScoreHabitationModule(StationRuntime station, SimulationWorld world, FactionEconomySnapshot economy)
|
||||
{
|
||||
return ScoreHabitationModule(station) + EstimateSupportUnlockScore(world, station, economy, "module_arg_hab_m_01");
|
||||
}
|
||||
|
||||
private static void AddOrRaiseCandidate(IDictionary<string, float> candidates, string moduleId, float score)
|
||||
{
|
||||
if (score <= 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!candidates.TryGetValue(moduleId, out var existing) || score > existing)
|
||||
{
|
||||
candidates[moduleId] = score;
|
||||
}
|
||||
}
|
||||
|
||||
private static float EstimateMarginalOutputRate(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
string moduleId,
|
||||
string commodityId)
|
||||
{
|
||||
var recipe = world.Recipes.Values
|
||||
.Where(recipe =>
|
||||
string.Equals(StationSimulationService.GetStationProductionLaneKey(world, recipe), moduleId, StringComparison.Ordinal)
|
||||
&& StationSimulationService.RecipeAppliesToStation(station, recipe))
|
||||
.Where(recipe => recipe.Outputs.Any(output => string.Equals(output.ItemId, commodityId, StringComparison.Ordinal)))
|
||||
.OrderByDescending(recipe => recipe.Priority)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (recipe is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var amount = recipe.Outputs
|
||||
.Where(output => string.Equals(output.ItemId, commodityId, StringComparison.Ordinal))
|
||||
.Sum(output => output.Amount);
|
||||
return amount * station.WorkforceEffectiveRatio / MathF.Max(recipe.Duration, 0.01f);
|
||||
}
|
||||
|
||||
private static float EstimateObjectiveExpansionFeasibility(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
FactionEconomySnapshot economy,
|
||||
string moduleId,
|
||||
string commodityId)
|
||||
{
|
||||
var recipes = world.Recipes.Values
|
||||
.Where(recipe =>
|
||||
string.Equals(StationSimulationService.GetStationProductionLaneKey(world, recipe), moduleId, StringComparison.Ordinal)
|
||||
&& StationSimulationService.RecipeAppliesToStation(station, recipe)
|
||||
&& recipe.Outputs.Any(output => string.Equals(output.ItemId, commodityId, StringComparison.Ordinal)))
|
||||
.ToList();
|
||||
if (recipes.Count == 0)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
var feasibility = 1f;
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
var inputCommodity = economy.GetCommodity(input.ItemId);
|
||||
if (inputCommodity.AvailableStock <= 0.01f && inputCommodity.ProjectedProductionRatePerSecond <= 0.01f)
|
||||
{
|
||||
feasibility *= 0.65f;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!float.IsPositiveInfinity(inputCommodity.ProjectedShortageHorizonSeconds)
|
||||
&& inputCommodity.ProjectedShortageHorizonSeconds < 180f)
|
||||
{
|
||||
feasibility *= 0.82f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Math.Clamp(feasibility, 0.35f, 1.15f);
|
||||
}
|
||||
|
||||
private static float EstimateProducerReadiness(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
FactionEconomySnapshot economy,
|
||||
string moduleId,
|
||||
string commodityId)
|
||||
{
|
||||
var analysis = AnalyzeProducerLane(world, station, economy, moduleId, commodityId);
|
||||
return analysis.Readiness;
|
||||
}
|
||||
|
||||
private static ProducerLaneAnalysis AnalyzeProducerLane(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
FactionEconomySnapshot economy,
|
||||
string moduleId,
|
||||
string commodityId)
|
||||
{
|
||||
var recipe = world.Recipes.Values
|
||||
.Where(recipe =>
|
||||
string.Equals(StationSimulationService.GetStationProductionLaneKey(world, recipe), moduleId, StringComparison.Ordinal)
|
||||
&& StationSimulationService.RecipeAppliesToStation(station, recipe)
|
||||
&& recipe.Outputs.Any(output => string.Equals(output.ItemId, commodityId, StringComparison.Ordinal)))
|
||||
.OrderByDescending(recipe => recipe.Priority)
|
||||
.FirstOrDefault();
|
||||
if (recipe is null)
|
||||
{
|
||||
return new ProducerLaneAnalysis(1f, 1f, false, false, false, false);
|
||||
}
|
||||
|
||||
var workforceFactor = station.WorkforceEffectiveRatio < 0.45f
|
||||
? 0.75f
|
||||
: station.WorkforceEffectiveRatio < 0.75f
|
||||
? 0.88f
|
||||
: 1f;
|
||||
var inputFactor = 1f;
|
||||
var missingLocalInputs = false;
|
||||
var missingFactionInputs = false;
|
||||
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
var localAmount = GetInventoryAmount(station.Inventory, input.ItemId);
|
||||
var commodity = economy.GetCommodity(input.ItemId);
|
||||
if (localAmount + 0.001f >= input.Amount)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
missingLocalInputs = true;
|
||||
var shortage = input.Amount - localAmount;
|
||||
var availableStockRatio = commodity.AvailableStock <= 0.01f ? 0f : MathF.Min(1f, commodity.AvailableStock / MathF.Max(input.Amount, 0.01f));
|
||||
if (commodity.AvailableStock >= shortage)
|
||||
{
|
||||
inputFactor *= 0.95f + (availableStockRatio * 0.05f);
|
||||
}
|
||||
else if (commodity.ProjectedProductionRatePerSecond > 0.01f)
|
||||
{
|
||||
inputFactor *= 0.82f + (availableStockRatio * 0.08f);
|
||||
}
|
||||
else
|
||||
{
|
||||
inputFactor *= 0.55f + (availableStockRatio * 0.15f);
|
||||
missingFactionInputs = true;
|
||||
}
|
||||
}
|
||||
|
||||
var outputReady = true;
|
||||
foreach (var output in recipe.Outputs)
|
||||
{
|
||||
if (!CanStationAcceptStationOutputSoon(world, station, output.ItemId, output.Amount))
|
||||
{
|
||||
outputReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
var readiness = Math.Clamp(workforceFactor * inputFactor * (outputReady ? 1f : 0.72f), 0.4f, 1.1f);
|
||||
return new ProducerLaneAnalysis(
|
||||
readiness,
|
||||
workforceFactor,
|
||||
missingLocalInputs,
|
||||
missingFactionInputs,
|
||||
!outputReady,
|
||||
outputReady && inputFactor >= 0.9f);
|
||||
}
|
||||
|
||||
private static float EstimateSupportUnlockScore(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
FactionEconomySnapshot? economy,
|
||||
string supportModuleId)
|
||||
{
|
||||
var role = StationSimulationService.DetermineStationRole(station);
|
||||
var objectiveCommodityId = GetObjectiveCommodityId(role);
|
||||
var objectiveModuleId = GetObjectiveModuleId(world, role, objectiveCommodityId);
|
||||
if (string.IsNullOrWhiteSpace(objectiveCommodityId) || string.IsNullOrWhiteSpace(objectiveModuleId))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var analysis = economy is null
|
||||
? new ProducerLaneAnalysis(0.75f, 1f, false, false, false, false)
|
||||
: AnalyzeProducerLane(world, station, economy, objectiveModuleId, objectiveCommodityId);
|
||||
|
||||
var unlockScore = 0f;
|
||||
switch (supportModuleId)
|
||||
{
|
||||
case "module_arg_hab_m_01" when analysis.WorkforceFactor < 0.9f
|
||||
&& !analysis.HasMissingFactionInputs
|
||||
&& !analysis.HasMissingOutputStorage:
|
||||
unlockScore += (1f - analysis.WorkforceFactor) * 150f;
|
||||
break;
|
||||
case "module_gen_prod_energycells_01":
|
||||
if (ObjectiveNeedsEnergy(world, objectiveCommodityId)
|
||||
&& analysis.HasMissingLocalInputs
|
||||
&& (economy?.GetCommodity("energycells").AvailableStock ?? 0f) < 120f)
|
||||
{
|
||||
unlockScore += 90f;
|
||||
}
|
||||
break;
|
||||
case "module_arg_stor_container_m_01":
|
||||
case "module_arg_stor_solid_m_01":
|
||||
case "module_arg_stor_liquid_m_01":
|
||||
var storageClass = supportModuleId switch
|
||||
{
|
||||
"module_arg_stor_solid_m_01" => "solid",
|
||||
"module_arg_stor_liquid_m_01" => "liquid",
|
||||
_ => "container",
|
||||
};
|
||||
if (analysis.HasMissingOutputStorage
|
||||
&& (ModuleNeedsStorageClass(world, objectiveModuleId, storageClass)
|
||||
|| CommodityUsesStorageClass(world, objectiveCommodityId, storageClass)))
|
||||
{
|
||||
unlockScore += 70f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return unlockScore * MathF.Max(0.4f, 1f - analysis.Readiness);
|
||||
}
|
||||
|
||||
private static float EstimateImmediateProducerActivationScore(
|
||||
SimulationWorld world,
|
||||
StationRuntime station,
|
||||
FactionEconomySnapshot economy,
|
||||
string moduleId,
|
||||
string commodityId)
|
||||
{
|
||||
var analysis = AnalyzeProducerLane(world, station, economy, moduleId, commodityId);
|
||||
if (analysis.CanRunSoon)
|
||||
{
|
||||
return 110f;
|
||||
}
|
||||
|
||||
if (!analysis.HasMissingFactionInputs && !analysis.HasMissingOutputStorage)
|
||||
{
|
||||
return 45f * MathF.Max(0.6f, analysis.WorkforceFactor);
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static float EstimateConstructionBottleneckImpact(
|
||||
SimulationWorld world,
|
||||
string moduleId,
|
||||
IReadOnlyDictionary<string, float> constructionDemandByItem)
|
||||
{
|
||||
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var score = 0f;
|
||||
foreach (var productItemId in moduleDefinition.Products)
|
||||
{
|
||||
if (!constructionDemandByItem.TryGetValue(productItemId, out var outstandingDemand) || outstandingDemand <= 0.01f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var outputRate = EstimateModuleOutputRate(world, moduleId, productItemId);
|
||||
if (outputRate <= 0.0001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
score += MathF.Min(outstandingDemand, outputRate * 900f) * 0.8f;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static float EstimateModuleOutputRate(SimulationWorld world, string moduleId, string itemId)
|
||||
{
|
||||
var recipe = world.Recipes.Values
|
||||
.Where(recipe => string.Equals(StationSimulationService.GetStationProductionLaneKey(world, recipe), moduleId, StringComparison.Ordinal))
|
||||
.Where(recipe => recipe.Outputs.Any(output => string.Equals(output.ItemId, itemId, StringComparison.Ordinal)))
|
||||
.OrderByDescending(recipe => recipe.Priority)
|
||||
.FirstOrDefault();
|
||||
if (recipe is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return recipe.Outputs
|
||||
.Where(output => string.Equals(output.ItemId, itemId, StringComparison.Ordinal))
|
||||
.Sum(output => output.Amount) / MathF.Max(recipe.Duration, 0.01f);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, float> GetOutstandingConstructionDemand(SimulationWorld world, string factionId)
|
||||
{
|
||||
var demand = new Dictionary<string, float>(StringComparer.Ordinal);
|
||||
|
||||
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))
|
||||
{
|
||||
foreach (var required in site.RequiredItems)
|
||||
{
|
||||
var remaining = MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key));
|
||||
if (remaining <= 0.01f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
demand[required.Key] = demand.GetValueOrDefault(required.Key) + remaining;
|
||||
}
|
||||
}
|
||||
|
||||
return demand;
|
||||
}
|
||||
|
||||
private static bool ModuleNeedsStorageClass(SimulationWorld world, string moduleId, string storageClass)
|
||||
{
|
||||
if (!world.ModuleRecipes.TryGetValue(moduleId, out var recipe))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return recipe.Inputs.Any(input =>
|
||||
world.ItemDefinitions.TryGetValue(input.ItemId, out var itemDefinition)
|
||||
&& string.Equals(itemDefinition.CargoKind, storageClass, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static bool CommodityUsesStorageClass(SimulationWorld world, string commodityId, string storageClass) =>
|
||||
world.ItemDefinitions.TryGetValue(commodityId, out var itemDefinition)
|
||||
&& string.Equals(itemDefinition.CargoKind, storageClass, StringComparison.Ordinal);
|
||||
|
||||
private static bool CanStationAcceptStationOutputSoon(SimulationWorld world, StationRuntime station, string itemId, float amount)
|
||||
{
|
||||
if (!world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var capacity = GetStationStorageCapacity(station, itemDefinition.CargoKind);
|
||||
if (capacity <= 0.01f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var used = station.Inventory
|
||||
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && string.Equals(definition.CargoKind, itemDefinition.CargoKind, StringComparison.Ordinal))
|
||||
.Sum(entry => entry.Value);
|
||||
return used + amount <= capacity * 0.95f;
|
||||
}
|
||||
|
||||
private static bool ObjectiveNeedsEnergy(SimulationWorld world, string objectiveCommodityId) =>
|
||||
world.ProductionGraph.GetImmediateInputs(objectiveCommodityId).Contains("energycells", StringComparer.Ordinal);
|
||||
|
||||
internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
|
||||
{
|
||||
var nextModuleId = GetNextStationModuleToBuild(station, world);
|
||||
@@ -223,6 +774,16 @@ internal sealed class InfrastructureSimulationService
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record ModuleExpansionCandidate(string ModuleId, float Score);
|
||||
|
||||
private sealed record ProducerLaneAnalysis(
|
||||
float Readiness,
|
||||
float WorkforceFactor,
|
||||
bool HasMissingLocalInputs,
|
||||
bool HasMissingFactionInputs,
|
||||
bool HasMissingOutputStorage,
|
||||
bool CanRunSoon);
|
||||
|
||||
internal static int GetDockingPadCount(StationRuntime station) =>
|
||||
CountModules(station.InstalledModules, "module_arg_dock_m_01_lowtech") * 2;
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ internal sealed class StationLifecycleService
|
||||
};
|
||||
|
||||
world.Ships.Add(ship);
|
||||
EnsureSpawnedShipCommander(world, station, ship);
|
||||
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
|
||||
{
|
||||
faction.ShipsBuilt += 1;
|
||||
@@ -124,4 +125,86 @@ internal sealed class StationLifecycleService
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
internal static void EnsureStationCommander(SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(station.CommanderId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var factionCommander = world.Commanders.FirstOrDefault(candidate =>
|
||||
string.Equals(candidate.Kind, CommanderKind.Faction, StringComparison.Ordinal)
|
||||
&& string.Equals(candidate.FactionId, station.FactionId, StringComparison.Ordinal));
|
||||
var faction = world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, station.FactionId, StringComparison.Ordinal));
|
||||
if (factionCommander is null || faction is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var commander = new CommanderRuntime
|
||||
{
|
||||
Id = $"commander-station-{station.Id}",
|
||||
Kind = CommanderKind.Station,
|
||||
FactionId = station.FactionId,
|
||||
ParentCommanderId = factionCommander.Id,
|
||||
ControlledEntityId = station.Id,
|
||||
PolicySetId = factionCommander.PolicySetId,
|
||||
Doctrine = "station-default",
|
||||
};
|
||||
|
||||
station.CommanderId = commander.Id;
|
||||
station.PolicySetId = factionCommander.PolicySetId;
|
||||
factionCommander.SubordinateCommanderIds.Add(commander.Id);
|
||||
faction.CommanderIds.Add(commander.Id);
|
||||
world.Commanders.Add(commander);
|
||||
}
|
||||
|
||||
private static void EnsureSpawnedShipCommander(SimulationWorld world, StationRuntime station, ShipRuntime ship)
|
||||
{
|
||||
var factionCommander = world.Commanders.FirstOrDefault(candidate =>
|
||||
string.Equals(candidate.Kind, CommanderKind.Faction, StringComparison.Ordinal)
|
||||
&& string.Equals(candidate.FactionId, station.FactionId, StringComparison.Ordinal));
|
||||
var faction = world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, station.FactionId, StringComparison.Ordinal));
|
||||
if (factionCommander is null || faction is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var commander = new CommanderRuntime
|
||||
{
|
||||
Id = $"commander-ship-{ship.Id}",
|
||||
Kind = CommanderKind.Ship,
|
||||
FactionId = ship.FactionId,
|
||||
ParentCommanderId = factionCommander.Id,
|
||||
ControlledEntityId = ship.Id,
|
||||
PolicySetId = factionCommander.PolicySetId,
|
||||
Doctrine = "ship-default",
|
||||
ActiveBehavior = new CommanderBehaviorRuntime
|
||||
{
|
||||
Kind = ship.DefaultBehavior.Kind,
|
||||
AreaSystemId = ship.DefaultBehavior.AreaSystemId,
|
||||
TargetEntityId = ship.DefaultBehavior.TargetEntityId,
|
||||
ItemId = ship.DefaultBehavior.ItemId,
|
||||
StationId = ship.DefaultBehavior.StationId,
|
||||
ModuleId = ship.DefaultBehavior.ModuleId,
|
||||
NodeId = ship.DefaultBehavior.NodeId,
|
||||
Phase = ship.DefaultBehavior.Phase,
|
||||
PatrolIndex = ship.DefaultBehavior.PatrolIndex,
|
||||
},
|
||||
ActiveTask = new CommanderTaskRuntime
|
||||
{
|
||||
Kind = ShipTaskKinds.Idle,
|
||||
Status = WorkStatus.Pending,
|
||||
TargetSystemId = ship.SystemId,
|
||||
Threshold = 0f,
|
||||
},
|
||||
};
|
||||
|
||||
ship.CommanderId = commander.Id;
|
||||
ship.PolicySetId = factionCommander.PolicySetId;
|
||||
factionCommander.SubordinateCommanderIds.Add(commander.Id);
|
||||
faction.CommanderIds.Add(commander.Id);
|
||||
world.Commanders.Add(commander);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,23 +15,54 @@ internal sealed class StationSimulationService
|
||||
}
|
||||
|
||||
var desiredOrders = new List<DesiredMarketOrder>();
|
||||
var economy = FactionEconomyAnalyzer.Build(world, station.FactionId);
|
||||
var role = DetermineStationRole(station);
|
||||
var site = GetConstructionSiteForStation(world, station.Id);
|
||||
var waterReserve = MathF.Max(30f, station.Population * 3f);
|
||||
var refinedReserve = HasStationModules(station, "module_gen_prod_hullparts_01") ? 140f : 40f;
|
||||
var oreReserve = HasRefineryCapability(station) ? 180f : 0f;
|
||||
var shipPartsReserve = HasStationModules(station, "module_gen_prod_hullparts_01")
|
||||
&& !HasStationModules(station, "module_gen_prod_advancedelectronics_01", "module_gen_build_l_01")
|
||||
var constructionEnergyReserve = GetConstructionDemandForItem(world, site, "energycells");
|
||||
var constructionHullpartsReserve = GetConstructionDemandForItem(world, site, "hullparts");
|
||||
var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics");
|
||||
var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals");
|
||||
var iceReserve = role == "water" ? 260f : 0f;
|
||||
var energyReserve = role switch
|
||||
{
|
||||
"power" => 120f,
|
||||
"refinery" => 160f,
|
||||
"hullparts" => 180f,
|
||||
"claytronics" => 220f,
|
||||
"water" => 140f,
|
||||
_ => 60f,
|
||||
} + constructionEnergyReserve;
|
||||
var refinedReserve = role switch
|
||||
{
|
||||
"hullparts" => 220f,
|
||||
"shipyard" => 260f,
|
||||
"refinery" => 80f,
|
||||
_ => 0f,
|
||||
};
|
||||
var oreReserve = role == "refinery" ? 260f : 0f;
|
||||
var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
||||
var claytronicsReserve = MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
||||
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
||||
&& FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships")
|
||||
? 90f
|
||||
: 0f;
|
||||
|
||||
AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f);
|
||||
AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f);
|
||||
AddDemandOrder(desiredOrders, station, "refinedmetals", refinedReserve, valuationBase: 1.15f);
|
||||
AddDemandOrder(desiredOrders, station, "hullparts", shipPartsReserve, valuationBase: 1.3f);
|
||||
AddDemandOrder(desiredOrders, station, "water", ScaleReserveByEconomy(economy, "water", waterReserve), valuationBase: ScaleDemandValuation(economy, "water", 1.1f));
|
||||
AddDemandOrder(desiredOrders, station, "energycells", ScaleReserveByEconomy(economy, "energycells", energyReserve), valuationBase: ScaleDemandValuation(economy, "energycells", 1.0f));
|
||||
AddDemandOrder(desiredOrders, station, "ice", ScaleReserveByEconomy(economy, "ice", iceReserve), valuationBase: ScaleDemandValuation(economy, "ice", 1.0f));
|
||||
AddDemandOrder(desiredOrders, station, "ore", ScaleReserveByEconomy(economy, "ore", oreReserve), valuationBase: ScaleDemandValuation(economy, "ore", 1.0f));
|
||||
AddDemandOrder(desiredOrders, station, "refinedmetals", ScaleReserveByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve)), valuationBase: ScaleDemandValuation(economy, "refinedmetals", 1.15f));
|
||||
AddDemandOrder(desiredOrders, station, "hullparts", ScaleReserveByEconomy(economy, "hullparts", hullpartsReserve + shipPartsReserve), valuationBase: ScaleDemandValuation(economy, "hullparts", 1.3f));
|
||||
AddDemandOrder(desiredOrders, station, "claytronics", ScaleReserveByEconomy(economy, "claytronics", claytronicsReserve), valuationBase: ScaleDemandValuation(economy, "claytronics", 1.35f));
|
||||
|
||||
AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f);
|
||||
AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f);
|
||||
AddSupplyOrder(desiredOrders, station, "refinedmetals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
|
||||
AddSupplyOrder(desiredOrders, station, "water", ScaleSupplyTriggerByEconomy(economy, "water", waterReserve * 1.5f), reserveFloor: waterReserve, valuationBase: ScaleSupplyValuation(economy, "water", 0.65f));
|
||||
AddSupplyOrder(desiredOrders, station, "energycells", ScaleSupplyTriggerByEconomy(economy, "energycells", energyReserve * 1.4f), reserveFloor: energyReserve, valuationBase: ScaleSupplyValuation(economy, "energycells", 0.7f));
|
||||
AddSupplyOrder(desiredOrders, station, "ice", ScaleSupplyTriggerByEconomy(economy, "ice", iceReserve * 1.4f), reserveFloor: iceReserve, valuationBase: ScaleSupplyValuation(economy, "ice", 0.5f));
|
||||
AddSupplyOrder(desiredOrders, station, "ore", ScaleSupplyTriggerByEconomy(economy, "ore", oreReserve * 1.4f), reserveFloor: oreReserve, valuationBase: ScaleSupplyValuation(economy, "ore", 0.7f));
|
||||
AddSupplyOrder(desiredOrders, station, "refinedmetals", ScaleSupplyTriggerByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve) * 1.4f), reserveFloor: MathF.Max(refinedReserve, constructionRefinedReserve), valuationBase: ScaleSupplyValuation(economy, "refinedmetals", 0.95f));
|
||||
AddSupplyOrder(desiredOrders, station, "hullparts", ScaleSupplyTriggerByEconomy(economy, "hullparts", MathF.Max(hullpartsReserve * 1.35f, hullpartsReserve + 40f)), reserveFloor: hullpartsReserve, valuationBase: ScaleSupplyValuation(economy, "hullparts", 1.05f));
|
||||
AddSupplyOrder(desiredOrders, station, "claytronics", ScaleSupplyTriggerByEconomy(economy, "claytronics", MathF.Max(claytronicsReserve * 1.35f, claytronicsReserve + 30f)), reserveFloor: claytronicsReserve, valuationBase: ScaleSupplyValuation(economy, "claytronics", 1.1f));
|
||||
|
||||
ReconcileStationMarketOrders(world, station, desiredOrders);
|
||||
}
|
||||
@@ -112,11 +143,11 @@ internal sealed class StationSimulationService
|
||||
.OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe))
|
||||
.FirstOrDefault(recipe => CanRunRecipe(world, station, recipe));
|
||||
|
||||
private static string? GetStationProductionLaneKey(SimulationWorld world, RecipeDefinition recipe) =>
|
||||
internal static string? GetStationProductionLaneKey(SimulationWorld world, RecipeDefinition recipe) =>
|
||||
recipe.RequiredModules.FirstOrDefault(moduleId =>
|
||||
world.ModuleDefinitions.TryGetValue(moduleId, out var def) && !string.IsNullOrEmpty(def.ProductionMode));
|
||||
|
||||
private static float GetStationProductionThroughput(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
|
||||
internal static float GetStationProductionThroughput(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
var laneModuleId = GetStationProductionLaneKey(world, recipe);
|
||||
if (laneModuleId is null)
|
||||
@@ -180,7 +211,7 @@ internal sealed class StationSimulationService
|
||||
};
|
||||
}
|
||||
|
||||
private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
|
||||
internal static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
|
||||
|| string.Equals(recipe.FacilityCategory, "farm", StringComparison.Ordinal)
|
||||
@@ -240,6 +271,71 @@ internal sealed class StationSimulationService
|
||||
private static bool HasRefineryCapability(StationRuntime station) =>
|
||||
HasStationModules(station, "module_gen_prod_refinedmetals_01", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01");
|
||||
|
||||
internal static string NormalizeStationObjective(string? objective)
|
||||
{
|
||||
return objective?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"power" or "energy" or "energycells" => "power",
|
||||
"water" or "ice-refinery" => "water",
|
||||
"refinery" or "refinedmetals" => "refinery",
|
||||
"hullparts" or "hull" => "hullparts",
|
||||
"claytronics" or "clay" => "claytronics",
|
||||
"shipyard" or "ship-production" => "shipyard",
|
||||
_ => "general",
|
||||
};
|
||||
}
|
||||
|
||||
internal static string DetermineStationRole(StationRuntime station)
|
||||
{
|
||||
var objective = NormalizeStationObjective(station.Objective);
|
||||
if (!string.Equals(objective, "general", StringComparison.Ordinal))
|
||||
{
|
||||
return objective;
|
||||
}
|
||||
|
||||
if (HasStationModules(station, "module_gen_build_l_01"))
|
||||
{
|
||||
return "shipyard";
|
||||
}
|
||||
|
||||
if (HasStationModules(station, "module_gen_prod_water_01"))
|
||||
{
|
||||
return "water";
|
||||
}
|
||||
|
||||
if (HasStationModules(station, "module_gen_prod_claytronics_01"))
|
||||
{
|
||||
return "claytronics";
|
||||
}
|
||||
|
||||
if (HasStationModules(station, "module_gen_prod_hullparts_01"))
|
||||
{
|
||||
return "hullparts";
|
||||
}
|
||||
|
||||
if (HasStationModules(station, "module_gen_prod_refinedmetals_01"))
|
||||
{
|
||||
return "refinery";
|
||||
}
|
||||
|
||||
if (HasStationModules(station, "module_gen_prod_energycells_01"))
|
||||
{
|
||||
return "power";
|
||||
}
|
||||
|
||||
return "general";
|
||||
}
|
||||
|
||||
private static float GetConstructionDemandForItem(SimulationWorld world, ConstructionSiteRuntime? site, string itemId)
|
||||
{
|
||||
if (site is null || !site.RequiredItems.TryGetValue(itemId, out var required))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, itemId));
|
||||
}
|
||||
|
||||
private static void AddDemandOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase)
|
||||
{
|
||||
var current = GetInventoryAmount(station.Inventory, itemId);
|
||||
@@ -267,7 +363,9 @@ internal sealed class StationSimulationService
|
||||
return;
|
||||
}
|
||||
|
||||
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Sell, itemId, surplus, valuationBase, reserveFloor));
|
||||
var surplusRatio = triggerAmount <= 0.01f ? 1f : MathF.Min(1f, surplus / triggerAmount);
|
||||
var liquidationValuation = MathF.Max(0.05f, valuationBase * (1f - (0.85f * surplusRatio)));
|
||||
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Sell, itemId, surplus, liquidationValuation, reserveFloor));
|
||||
}
|
||||
|
||||
private static void ReconcileStationMarketOrders(SimulationWorld world, StationRuntime station, IReadOnlyCollection<DesiredMarketOrder> desiredOrders)
|
||||
@@ -330,6 +428,50 @@ internal sealed class StationSimulationService
|
||||
return world.Systems.Count(system => FactionControlsSystem(world, factionId, system.Definition.Id));
|
||||
}
|
||||
|
||||
private static float ScaleReserveByEconomy(FactionEconomySnapshot economy, string itemId, float baseReserve)
|
||||
{
|
||||
var commodity = economy.GetCommodity(itemId);
|
||||
if (float.IsPositiveInfinity(commodity.ShortageHorizonSeconds))
|
||||
{
|
||||
return MathF.Max(0f, baseReserve);
|
||||
}
|
||||
|
||||
return commodity.ShortageHorizonSeconds < 180f
|
||||
? baseReserve * 1.5f
|
||||
: commodity.ShortageHorizonSeconds < 360f
|
||||
? baseReserve * 1.2f
|
||||
: baseReserve;
|
||||
}
|
||||
|
||||
private static float ScaleSupplyTriggerByEconomy(FactionEconomySnapshot economy, string itemId, float baseTrigger)
|
||||
{
|
||||
var commodity = economy.GetCommodity(itemId);
|
||||
return commodity.NetRatePerSecond < -0.01f ? baseTrigger * 1.2f : baseTrigger;
|
||||
}
|
||||
|
||||
private static float ScaleDemandValuation(FactionEconomySnapshot economy, string itemId, float baseValuation)
|
||||
{
|
||||
var commodity = economy.GetCommodity(itemId);
|
||||
if (float.IsPositiveInfinity(commodity.ShortageHorizonSeconds))
|
||||
{
|
||||
return commodity.ProductionRatePerSecond > 0.01f ? baseValuation : baseValuation * 1.3f;
|
||||
}
|
||||
|
||||
return commodity.ShortageHorizonSeconds < 180f
|
||||
? baseValuation * 1.5f
|
||||
: commodity.ShortageHorizonSeconds < 360f
|
||||
? baseValuation * 1.25f
|
||||
: baseValuation;
|
||||
}
|
||||
|
||||
private static float ScaleSupplyValuation(FactionEconomySnapshot economy, string itemId, float baseValuation)
|
||||
{
|
||||
var commodity = economy.GetCommodity(itemId);
|
||||
return commodity.NetRatePerSecond > 0.01f && commodity.ShortageHorizonSeconds > 600f
|
||||
? baseValuation * 0.75f
|
||||
: baseValuation;
|
||||
}
|
||||
|
||||
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
||||
{
|
||||
var totalLagrangePoints = world.Celestials.Count(node =>
|
||||
|
||||
Reference in New Issue
Block a user