Deepen faction economy and station planning

This commit is contained in:
2026-03-19 23:34:06 -04:00
parent 9a5040cf1f
commit cd1fe776a5
33 changed files with 3170 additions and 175 deletions

View File

@@ -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 =>