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

@@ -20,6 +20,7 @@ internal sealed class DataCatalogLoader(string dataRoot)
var balance = Read<BalanceDefinition>("balance.json");
var recipes = BuildRecipes(items, ships, modules);
var moduleRecipes = BuildModuleRecipes(modules);
var productionGraph = ProductionGraphBuilder.Build(items, recipes, modules);
return new ScenarioCatalog(
authoredSystems,
@@ -29,7 +30,8 @@ internal sealed class DataCatalogLoader(string dataRoot)
ships.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
items.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
recipes.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
moduleRecipes.ToDictionary(definition => definition.ModuleId, StringComparer.Ordinal));
moduleRecipes.ToDictionary(definition => definition.ModuleId, StringComparer.Ordinal),
productionGraph);
}
internal ScenarioDefinition NormalizeScenarioToAvailableSystems(
@@ -56,6 +58,7 @@ internal sealed class DataCatalogLoader(string dataRoot)
SystemId = ResolveSystemId(station.SystemId),
Label = station.Label,
Color = station.Color,
Objective = station.Objective,
StartingModules = station.StartingModules.ToList(),
FactionId = station.FactionId,
PlanetIndex = station.PlanetIndex,
@@ -71,6 +74,7 @@ internal sealed class DataCatalogLoader(string dataRoot)
Center = formation.Center.ToArray(),
SystemId = ResolveSystemId(formation.SystemId),
FactionId = formation.FactionId,
StartingInventory = new Dictionary<string, float>(formation.StartingInventory, StringComparer.Ordinal),
})
.ToList(),
PatrolRoutes = scenario.PatrolRoutes
@@ -297,4 +301,5 @@ internal sealed record ScenarioCatalog(
IReadOnlyDictionary<string, ShipDefinition> ShipDefinitions,
IReadOnlyDictionary<string, ItemDefinition> ItemDefinitions,
IReadOnlyDictionary<string, RecipeDefinition> Recipes,
IReadOnlyDictionary<string, ModuleRecipeDefinition> ModuleRecipes);
IReadOnlyDictionary<string, ModuleRecipeDefinition> ModuleRecipes,
ProductionGraph ProductionGraph);

View File

@@ -39,7 +39,7 @@ internal sealed class WorldBuilder(
seedingService.InitializeStationStockpiles(stations);
var refinery = seedingService.SelectRefineryStation(stations, scenario);
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, refinery);
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery);
var factions = seedingService.CreateFactions(stations, ships);
seedingService.BootstrapFactionEconomy(factions, stations);
@@ -47,7 +47,32 @@ internal sealed class WorldBuilder(
var commanders = seedingService.CreateCommanders(factions, stations, ships);
var nowUtc = DateTimeOffset.UtcNow;
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(stations, claims, catalog.ModuleRecipes);
var bootstrapWorld = new SimulationWorld
{
Label = "Split Viewer / Bootstrap World",
Seed = WorldSeed,
Balance = catalog.Balance,
Systems = systemRuntimes,
Celestials = spatialLayout.Celestials,
Nodes = spatialLayout.Nodes,
Stations = stations,
Ships = ships,
Factions = factions,
Commanders = commanders,
Claims = claims,
ConstructionSites = [],
MarketOrders = [],
Policies = policies,
ShipDefinitions = new Dictionary<string, ShipDefinition>(catalog.ShipDefinitions, StringComparer.Ordinal),
ItemDefinitions = new Dictionary<string, ItemDefinition>(catalog.ItemDefinitions, StringComparer.Ordinal),
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(catalog.ModuleDefinitions, StringComparer.Ordinal),
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(catalog.ModuleRecipes, StringComparer.Ordinal),
Recipes = new Dictionary<string, RecipeDefinition>(catalog.Recipes, StringComparer.Ordinal),
ProductionGraph = catalog.ProductionGraph,
OrbitalTimeSeconds = WorldSeed * 97d,
GeneratedAtUtc = nowUtc,
};
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(bootstrapWorld);
return new SimulationWorld
{
@@ -70,6 +95,7 @@ internal sealed class WorldBuilder(
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(catalog.ModuleDefinitions, StringComparer.Ordinal),
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(catalog.ModuleRecipes, StringComparer.Ordinal),
Recipes = new Dictionary<string, RecipeDefinition>(catalog.Recipes, StringComparer.Ordinal),
ProductionGraph = catalog.ProductionGraph,
OrbitalTimeSeconds = WorldSeed * 97d,
GeneratedAtUtc = DateTimeOffset.UtcNow,
};
@@ -99,9 +125,12 @@ internal sealed class WorldBuilder(
SystemId = system.Definition.Id,
Label = plan.Label,
Color = plan.Color,
Objective = StationSimulationService.NormalizeStationObjective(plan.Objective),
Position = placement.Position,
FactionId = plan.FactionId ?? DefaultFactionId,
CelestialId = placement.AnchorCelestial.Id,
Health = 600f,
MaxHealth = 600f,
};
stations.Add(station);
@@ -142,6 +171,7 @@ internal sealed class WorldBuilder(
BalanceDefinition balance,
IReadOnlyDictionary<string, ShipDefinition> shipDefinitions,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
IReadOnlyCollection<StationRuntime> stations,
StationRuntime? refinery)
{
var ships = new List<ShipRuntime>();
@@ -168,10 +198,25 @@ internal sealed class WorldBuilder(
Position = position,
TargetPosition = position,
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials),
DefaultBehavior = WorldSeedingService.CreateBehavior(definition, formation.SystemId, scenario, patrolRoutes, refinery),
DefaultBehavior = WorldSeedingService.CreateBehavior(
definition,
formation.SystemId,
formation.FactionId ?? DefaultFactionId,
scenario,
patrolRoutes,
stations,
refinery),
ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending },
Health = definition.MaxHealth,
});
foreach (var (itemId, amount) in formation.StartingInventory)
{
if (amount > 0f)
{
ships[^1].Inventory[itemId] = amount;
}
}
}
}

View File

@@ -37,7 +37,7 @@ internal sealed class WorldSeedingService
.ToList();
var refineries = ownedStations
.Where(station => HasInstalledModules(station, "module_gen_prod_refinedmetals_01", "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01"))
.Where(station => string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal))
.ToList();
if (refineries.Count > 0)
@@ -65,10 +65,32 @@ internal sealed class WorldSeedingService
foreach (var station in stations)
{
InitializeStationPopulation(station);
station.Inventory["refinedmetals"] = 120f;
if (station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
{
station.Inventory["energycells"] = MathF.Max(GetInventoryAmount(station.Inventory, "energycells"), 240f);
}
if (station.InstalledModules.Contains("module_gen_prod_refinedmetals_01", StringComparer.Ordinal))
{
station.Inventory["ore"] = MathF.Max(GetInventoryAmount(station.Inventory, "ore"), 220f);
}
if (station.InstalledModules.Contains("module_gen_prod_hullparts_01", StringComparer.Ordinal))
{
station.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(station.Inventory, "refinedmetals"), 240f);
station.Inventory["graphene"] = MathF.Max(GetInventoryAmount(station.Inventory, "graphene"), 80f);
}
if (station.InstalledModules.Contains("module_gen_prod_claytronics_01", StringComparer.Ordinal))
{
station.Inventory["antimattercells"] = MathF.Max(GetInventoryAmount(station.Inventory, "antimattercells"), 90f);
station.Inventory["microchips"] = MathF.Max(GetInventoryAmount(station.Inventory, "microchips"), 120f);
station.Inventory["quantumtubes"] = MathF.Max(GetInventoryAmount(station.Inventory, "quantumtubes"), 90f);
}
if (station.Population > 0f)
{
station.Inventory["water"] = MathF.Max(80f, station.Population * 1.5f);
station.Inventory["water"] = MathF.Max(60f, station.Population * 1.5f);
}
}
}
@@ -76,10 +98,10 @@ internal sealed class WorldSeedingService
internal StationRuntime? SelectRefineryStation(IReadOnlyCollection<StationRuntime> stations, ScenarioDefinition scenario)
{
return stations.FirstOrDefault(station =>
HasInstalledModules(station, "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01") &&
string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal) &&
station.SystemId == scenario.MiningDefaults.RefinerySystemId)
?? stations.FirstOrDefault(station =>
HasInstalledModules(station, "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01"));
string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal));
}
internal List<ClaimRuntime> CreateClaims(
@@ -116,23 +138,21 @@ internal sealed class WorldSeedingService
}
internal (List<ConstructionSiteRuntime> ConstructionSites, List<MarketOrderRuntime> MarketOrders) CreateConstructionSites(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<ClaimRuntime> claims,
IReadOnlyDictionary<string, ModuleRecipeDefinition> moduleRecipes)
SimulationWorld world)
{
var sites = new List<ConstructionSiteRuntime>();
var orders = new List<MarketOrderRuntime>();
foreach (var station in stations)
foreach (var station in world.Stations)
{
var moduleId = GetNextConstructionSiteModule(station, moduleRecipes);
var moduleId = InfrastructureSimulationService.GetNextStationModuleToBuild(station, world);
if (moduleId is null || station.CelestialId is null)
{
continue;
}
var claim = claims.FirstOrDefault(candidate => candidate.CelestialId == station.CelestialId);
if (claim is null || !moduleRecipes.TryGetValue(moduleId, out var recipe))
var claim = world.Claims.FirstOrDefault(candidate => candidate.CelestialId == station.CelestialId);
if (claim is null || !world.ModuleRecipes.TryGetValue(moduleId, out var recipe))
{
continue;
}
@@ -294,23 +314,40 @@ internal sealed class WorldSeedingService
internal static DefaultBehaviorRuntime CreateBehavior(
ShipDefinition definition,
string systemId,
string factionId,
ScenarioDefinition scenario,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
IReadOnlyCollection<StationRuntime> stations,
StationRuntime? refinery)
{
if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && refinery is not null)
var homeStation = stations.FirstOrDefault(station =>
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
&& string.Equals(station.SystemId, systemId, StringComparison.Ordinal))
?? stations.FirstOrDefault(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal))
?? refinery;
if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && homeStation is not null)
{
return new DefaultBehaviorRuntime
{
Kind = "construct-station",
StationId = refinery.Id,
StationId = homeStation.Id,
Phase = "travel-to-station",
};
}
if (HasCapabilities(definition, "mining") && refinery is not null)
if (HasCapabilities(definition, "mining") && homeStation is not null)
{
return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, refinery.Id);
return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, homeStation.Id);
}
if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal))
{
return new DefaultBehaviorRuntime
{
Kind = "trade-haul",
Phase = "travel-to-source",
};
}
if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && patrolRoutes.TryGetValue(systemId, out var route))
@@ -318,6 +355,7 @@ internal sealed class WorldSeedingService
return new DefaultBehaviorRuntime
{
Kind = "patrol",
StationId = homeStation?.Id,
PatrolPoints = route,
PatrolIndex = 0,
};
@@ -340,6 +378,13 @@ internal sealed class WorldSeedingService
Color = "#7ed4ff",
Credits = MinimumFactionCredits,
},
"asterion-league" => new FactionRuntime
{
Id = factionId,
Label = "Asterion League",
Color = "#ff8f70",
Credits = MinimumFactionCredits,
},
_ => new FactionRuntime
{
Id = factionId,
@@ -350,31 +395,6 @@ internal sealed class WorldSeedingService
};
}
private static string? GetNextConstructionSiteModule(
StationRuntime station,
IReadOnlyDictionary<string, ModuleRecipeDefinition> moduleRecipes)
{
foreach (var (moduleId, targetCount) in new (string ModuleId, int TargetCount)[]
{
("module_gen_prod_refinedmetals_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),
})
{
if (CountModules(station.InstalledModules, moduleId) < targetCount
&& moduleRecipes.ContainsKey(moduleId))
{
return moduleId;
}
}
return null;
}
private static void InitializeStationPopulation(StationRuntime station)
{
var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01");
@@ -406,6 +426,8 @@ internal sealed class WorldSeedingService
{
Kind = behavior.Kind,
AreaSystemId = behavior.AreaSystemId,
TargetEntityId = behavior.TargetEntityId,
ItemId = behavior.ItemId,
ModuleId = behavior.ModuleId,
NodeId = behavior.NodeId,
Phase = behavior.Phase,