Refactor world bootstrap and allow empty startup worlds
This commit is contained in:
@@ -5,7 +5,6 @@ namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
internal static class LoaderSupport
|
||||
{
|
||||
internal const string DefaultFactionId = "sol-dominion";
|
||||
internal const float MinimumFactionCredits = 0f;
|
||||
internal const float MinimumRefineryOre = 0f;
|
||||
internal const float MinimumRefineryStock = 0f;
|
||||
@@ -97,7 +96,7 @@ internal static class LoaderSupport
|
||||
{
|
||||
if (!moduleDefinitions.TryGetValue(moduleId, out var definition))
|
||||
{
|
||||
return;
|
||||
throw new InvalidOperationException($"Module '{moduleId}' is not defined in static data.");
|
||||
}
|
||||
|
||||
station.Modules.Add(StationModuleRuntime.Create($"{station.Id}-module-{station.Modules.Count + 1}", definition));
|
||||
|
||||
289
apps/backend/Universe/Scenario/ScenarioContentBuilder.cs
Normal file
289
apps/backend/Universe/Scenario/ScenarioContentBuilder.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
using SpaceGame.Api.Ships.Simulation;
|
||||
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class ScenarioContentBuilder(
|
||||
IStaticDataProvider staticData,
|
||||
IBalanceService balance)
|
||||
{
|
||||
public ScenarioWorldContent Build(
|
||||
ScenarioDefinition scenario,
|
||||
WorldBuildTopology topology)
|
||||
{
|
||||
var stations = CreateStations(
|
||||
scenario,
|
||||
topology.SystemsById,
|
||||
topology.SpatialLayout.SystemGraphs,
|
||||
topology.SpatialLayout.Celestials);
|
||||
|
||||
var patrolRoutes = BuildPatrolRoutes(scenario, topology.SystemsById);
|
||||
var ships = CreateShips(
|
||||
scenario,
|
||||
topology.SystemsById,
|
||||
topology.SpatialLayout.Celestials,
|
||||
patrolRoutes,
|
||||
stations);
|
||||
|
||||
return new ScenarioWorldContent(stations, ships);
|
||||
}
|
||||
|
||||
private List<StationRuntime> CreateStations(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials)
|
||||
{
|
||||
var stations = new List<StationRuntime>();
|
||||
var stationIdCounter = 0;
|
||||
|
||||
foreach (var plan in scenario.InitialStations)
|
||||
{
|
||||
if (!systemsById.TryGetValue(plan.SystemId, out var system))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario station '{plan.Label}' references unknown system '{plan.SystemId}'.");
|
||||
}
|
||||
|
||||
var placement = SpatialBuilder.ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], celestials);
|
||||
var station = new StationRuntime
|
||||
{
|
||||
Id = $"station-{++stationIdCounter}",
|
||||
SystemId = system.Definition.Id,
|
||||
Label = plan.Label,
|
||||
Color = plan.Color,
|
||||
Objective = StationSimulationService.NormalizeStationObjective(plan.Objective),
|
||||
Position = placement.Position,
|
||||
FactionId = GetRequiredFactionId(plan.FactionId, $"station '{plan.Label}'"),
|
||||
CelestialId = placement.AnchorCelestial.Id,
|
||||
Health = 600f,
|
||||
MaxHealth = 600f,
|
||||
};
|
||||
|
||||
stations.Add(station);
|
||||
placement.AnchorCelestial.OccupyingStructureId = station.Id;
|
||||
|
||||
var startingModules = BuildStartingModules(plan);
|
||||
foreach (var moduleId in startingModules)
|
||||
{
|
||||
AddStationModule(station, staticData.ModuleDefinitions, moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
return stations;
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> BuildStartingModules(InitialStationDefinition plan)
|
||||
{
|
||||
var startingModules = new List<string>(plan.StartingModules.Count > 0
|
||||
? plan.StartingModules
|
||||
: []);
|
||||
|
||||
EnsureStartingModule(startingModules, StarterStationLayoutResolver.ResolveDockModuleId(plan.FactionId, staticData.ModuleDefinitions));
|
||||
|
||||
var powerModuleId = StarterStationLayoutResolver.ResolvePowerModuleId(plan.FactionId, staticData.ModuleDefinitions);
|
||||
EnsureStartingModule(startingModules, powerModuleId);
|
||||
|
||||
var defaultContainerStorageModuleId = StarterStationLayoutResolver.ResolveRequiredStorageModuleIds(
|
||||
powerModuleId,
|
||||
plan.FactionId,
|
||||
staticData.ModuleDefinitions,
|
||||
staticData.ItemDefinitions)
|
||||
.FirstOrDefault(moduleId =>
|
||||
{
|
||||
return staticData.ModuleDefinitions.TryGetValue(moduleId, out var definition)
|
||||
&& definition is StorageModuleDefinition storageDefinition
|
||||
&& storageDefinition.StorageKind == StorageKind.Container;
|
||||
});
|
||||
|
||||
if (defaultContainerStorageModuleId is not null)
|
||||
{
|
||||
EnsureStartingModule(startingModules, defaultContainerStorageModuleId);
|
||||
}
|
||||
|
||||
var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(plan.Objective, plan.FactionId, staticData.ModuleDefinitions);
|
||||
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
|
||||
{
|
||||
EnsureStartingModule(startingModules, objectiveModuleId);
|
||||
|
||||
if (!string.Equals(objectiveModuleId, powerModuleId, StringComparison.Ordinal))
|
||||
{
|
||||
EnsureStartingModule(startingModules, powerModuleId);
|
||||
}
|
||||
|
||||
foreach (var storageModuleId in StarterStationLayoutResolver.ResolveRequiredStorageModuleIds(
|
||||
objectiveModuleId,
|
||||
plan.FactionId,
|
||||
staticData.ModuleDefinitions,
|
||||
staticData.ItemDefinitions))
|
||||
{
|
||||
EnsureStartingModule(startingModules, storageModuleId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var moduleId in startingModules)
|
||||
{
|
||||
if (!staticData.ModuleDefinitions.ContainsKey(moduleId))
|
||||
{
|
||||
throw new InvalidOperationException($"Station '{plan.Label}' requires module '{moduleId}', but it is not defined in static data.");
|
||||
}
|
||||
}
|
||||
|
||||
return startingModules;
|
||||
}
|
||||
|
||||
private static void EnsureStartingModule(List<string> modules, string moduleId)
|
||||
{
|
||||
if (!modules.Contains(moduleId, StringComparer.Ordinal))
|
||||
{
|
||||
modules.Add(moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById)
|
||||
{
|
||||
return scenario.PatrolRoutes
|
||||
.GroupBy(route => route.SystemId, StringComparer.Ordinal)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group
|
||||
.SelectMany(route => route.Points)
|
||||
.Select(point => NormalizeScenarioPoint(systemsById[group.Key], point))
|
||||
.ToList(),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private List<ShipRuntime> CreateShips(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
IReadOnlyCollection<StationRuntime> stations)
|
||||
{
|
||||
var ships = new List<ShipRuntime>();
|
||||
var shipIdCounter = 0;
|
||||
|
||||
foreach (var formation in scenario.ShipFormations)
|
||||
{
|
||||
if (!staticData.ShipDefinitions.TryGetValue(formation.ShipId, out var definition))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario ship formation references unknown ship '{formation.ShipId}'.");
|
||||
}
|
||||
|
||||
for (var index = 0; index < formation.Count; index += 1)
|
||||
{
|
||||
var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f);
|
||||
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
|
||||
var factionId = GetRequiredFactionId(formation.FactionId, $"ship formation '{formation.ShipId}' in system '{formation.SystemId}'");
|
||||
|
||||
ships.Add(new ShipRuntime
|
||||
{
|
||||
Id = $"ship-{++shipIdCounter}",
|
||||
SystemId = formation.SystemId,
|
||||
Definition = definition,
|
||||
FactionId = factionId,
|
||||
Position = position,
|
||||
TargetPosition = position,
|
||||
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials),
|
||||
DefaultBehavior = CreateBehavior(
|
||||
definition,
|
||||
formation.SystemId,
|
||||
factionId,
|
||||
patrolRoutes,
|
||||
stations),
|
||||
Skills = ShipBootstrapPolicy.CreateSkills(definition),
|
||||
Health = definition.MaxHealth,
|
||||
});
|
||||
|
||||
foreach (var (itemId, amount) in formation.StartingInventory)
|
||||
{
|
||||
if (amount > 0f)
|
||||
{
|
||||
ships[^1].Inventory[itemId] = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ships;
|
||||
}
|
||||
|
||||
private static string GetRequiredFactionId(string? factionId, string context)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(factionId))
|
||||
{
|
||||
return factionId;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Scenario {context} is missing a factionId.");
|
||||
}
|
||||
|
||||
private static DefaultBehaviorRuntime CreateBehavior(
|
||||
ShipDefinition definition,
|
||||
string systemId,
|
||||
string factionId,
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
IReadOnlyCollection<StationRuntime> stations)
|
||||
{
|
||||
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));
|
||||
|
||||
if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && homeStation is not null)
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "construct-station",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
PreferredConstructionSiteId = null,
|
||||
};
|
||||
}
|
||||
|
||||
if (LoaderSupport.HasCapabilities(definition, "mining") && homeStation is not null)
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
AreaSystemId = homeStation.SystemId,
|
||||
MaxSystemRange = definition.CargoCapacity >= 120f ? 3 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "advanced-auto-trade",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
MaxSystemRange = 2,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && patrolRoutes.TryGetValue(systemId, out var route))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "patrol",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
AreaSystemId = systemId,
|
||||
PatrolPoints = route,
|
||||
PatrolIndex = 0,
|
||||
};
|
||||
}
|
||||
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "idle",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,16 @@ public sealed class ScenarioLoader(IOptions<StaticDataOptions> staticDataOptions
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
public ScenarioDefinition? Load()
|
||||
public ScenarioDefinition Load(string relativePath)
|
||||
{
|
||||
var scenarioPath = Path.Combine(staticDataOptions.Value.DataRoot, "scenario.json");
|
||||
var scenarioPath = Path.Combine(staticDataOptions.Value.DataRoot, relativePath);
|
||||
if (!File.Exists(scenarioPath))
|
||||
{
|
||||
return null;
|
||||
throw new FileNotFoundException($"Scenario file was not found: {relativePath}", scenarioPath);
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(scenarioPath);
|
||||
return JsonSerializer.Deserialize<ScenarioDefinition>(json, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Unable to read scenario.json.");
|
||||
?? throw new InvalidOperationException($"Unable to read {relativePath}.");
|
||||
}
|
||||
}
|
||||
|
||||
111
apps/backend/Universe/Scenario/ScenarioValidationService.cs
Normal file
111
apps/backend/Universe/Scenario/ScenarioValidationService.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class ScenarioValidationService(IStaticDataProvider staticData)
|
||||
{
|
||||
public ScenarioDefinition CreateEmptyScenario(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
IReadOnlyList<SolarSystemDefinition> systems)
|
||||
{
|
||||
if (systems.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("World generation produced no systems.");
|
||||
}
|
||||
|
||||
return new ScenarioDefinition
|
||||
{
|
||||
WorldGeneration = worldGenerationOptions,
|
||||
InitialStations = [],
|
||||
ShipFormations = [],
|
||||
PatrolRoutes = [],
|
||||
};
|
||||
}
|
||||
|
||||
public void Validate(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlySet<string> availableSystemIds)
|
||||
{
|
||||
foreach (var station in scenario.InitialStations)
|
||||
{
|
||||
ValidateSystemExists(station.SystemId, $"station '{station.Label}' system", availableSystemIds);
|
||||
ValidateFactionId(station.FactionId, $"station '{station.Label}'");
|
||||
|
||||
foreach (var moduleId in station.StartingModules)
|
||||
{
|
||||
ValidateModuleId(moduleId, $"station '{station.Label}' starting module");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var formation in scenario.ShipFormations)
|
||||
{
|
||||
ValidateSystemExists(formation.SystemId, $"ship formation '{formation.ShipId}' system", availableSystemIds);
|
||||
ValidateFactionId(formation.FactionId, $"ship formation '{formation.ShipId}' in system '{formation.SystemId}'");
|
||||
ValidateShipId(formation.ShipId, $"ship formation in system '{formation.SystemId}'");
|
||||
|
||||
foreach (var itemId in formation.StartingInventory.Keys)
|
||||
{
|
||||
ValidateItemId(itemId, $"ship formation '{formation.ShipId}' starting inventory");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var route in scenario.PatrolRoutes)
|
||||
{
|
||||
ValidateSystemExists(route.SystemId, "patrol route system", availableSystemIds);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRequiredFactionId(string? factionId, string context)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(factionId))
|
||||
{
|
||||
return factionId;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Scenario {context} is missing a factionId.");
|
||||
}
|
||||
|
||||
private static void ValidateSystemExists(
|
||||
string systemId,
|
||||
string context,
|
||||
IReadOnlySet<string> availableSystemIds)
|
||||
{
|
||||
if (!availableSystemIds.Contains(systemId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown generated system '{systemId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateFactionId(string? factionId, string context)
|
||||
{
|
||||
var requiredFactionId = GetRequiredFactionId(factionId, context);
|
||||
if (!staticData.FactionDefinitions.ContainsKey(requiredFactionId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown faction '{requiredFactionId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateModuleId(string moduleId, string context)
|
||||
{
|
||||
if (!staticData.ModuleDefinitions.ContainsKey(moduleId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown module '{moduleId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateShipId(string shipId, string context)
|
||||
{
|
||||
if (!staticData.ShipDefinitions.ContainsKey(shipId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown ship '{shipId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateItemId(string itemId, string context)
|
||||
{
|
||||
if (!staticData.ItemDefinitions.ContainsKey(itemId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown item '{itemId}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
5
apps/backend/Universe/Scenario/ScenarioWorldContent.cs
Normal file
5
apps/backend/Universe/Scenario/ScenarioWorldContent.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed record ScenarioWorldContent(
|
||||
IReadOnlyList<StationRuntime> Stations,
|
||||
IReadOnlyList<ShipRuntime> Ships);
|
||||
@@ -2,9 +2,9 @@ using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class SpatialBuilder
|
||||
public sealed class SpatialBuilder(IBalanceService balance)
|
||||
{
|
||||
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems, BalanceOptions balance)
|
||||
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems)
|
||||
{
|
||||
var systemGraphs = systems.ToDictionary(
|
||||
system => system.Definition.Id,
|
||||
@@ -305,12 +305,12 @@ public sealed class SpatialBuilder
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record ScenarioSpatialLayout(
|
||||
public sealed record ScenarioSpatialLayout(
|
||||
IReadOnlyDictionary<string, SystemSpatialGraph> SystemGraphs,
|
||||
List<CelestialRuntime> Celestials,
|
||||
List<ResourceNodeRuntime> Nodes);
|
||||
|
||||
internal sealed record SystemSpatialGraph(
|
||||
public sealed record SystemSpatialGraph(
|
||||
string SystemId,
|
||||
List<CelestialRuntime> Celestials,
|
||||
Dictionary<int, Dictionary<string, CelestialRuntime>> LagrangeNodesByPlanetIndex);
|
||||
|
||||
145
apps/backend/Universe/Scenario/StarterStationLayoutResolver.cs
Normal file
145
apps/backend/Universe/Scenario/StarterStationLayoutResolver.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using SpaceGame.Api.Shared.Runtime;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
internal static class StarterStationLayoutResolver
|
||||
{
|
||||
internal static string ResolveDockModuleId(
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions) =>
|
||||
SelectPreferredModule(
|
||||
moduleDefinitions.Values.Where(definition => definition.ModuleType == ModuleType.DockArea),
|
||||
factionId,
|
||||
"starter dock module").Id;
|
||||
|
||||
internal static string ResolvePowerModuleId(
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions) =>
|
||||
ResolveProducerModuleId("energycells", factionId, moduleDefinitions);
|
||||
|
||||
internal static string? ResolveObjectiveModuleId(
|
||||
string? objective,
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
|
||||
{
|
||||
var targetWareId = ResolveObjectiveWareId(objective);
|
||||
return targetWareId is null ? null : ResolveProducerModuleId(targetWareId, factionId, moduleDefinitions);
|
||||
}
|
||||
|
||||
internal static IEnumerable<string> ResolveRequiredStorageModuleIds(
|
||||
string moduleId,
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||
{
|
||||
throw new InvalidOperationException($"Module '{moduleId}' is not defined in static data.");
|
||||
}
|
||||
|
||||
foreach (var wareId in moduleDefinition.BuildRecipes
|
||||
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||
.Concat(moduleDefinition.ProductItemIds)
|
||||
.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||
{
|
||||
throw new InvalidOperationException($"Module '{moduleId}' references unknown ware '{wareId}'.");
|
||||
}
|
||||
|
||||
if (itemDefinition.CargoKind is not { } storageKind)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return ResolveStorageModuleId(storageKind, factionId, moduleDefinitions);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveObjectiveWareId(string? objective) =>
|
||||
StationSimulationService.NormalizeStationObjective(objective) switch
|
||||
{
|
||||
"power" => "energycells",
|
||||
"refinery" => "refinedmetals",
|
||||
"graphene" => "graphene",
|
||||
"siliconwafers" => "siliconwafers",
|
||||
"hullparts" => "hullparts",
|
||||
"claytronics" => "claytronics",
|
||||
"quantumtubes" => "quantumtubes",
|
||||
"antimattercells" => "antimattercells",
|
||||
"superfluidcoolant" => "superfluidcoolant",
|
||||
"water" => "water",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static string ResolveProducerModuleId(
|
||||
string wareId,
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions) =>
|
||||
SelectPreferredModule(
|
||||
moduleDefinitions.Values
|
||||
.OfType<ProductionModuleDefinition>()
|
||||
.Where(definition => definition.ProductItemIds.Contains(wareId, StringComparer.Ordinal)),
|
||||
factionId,
|
||||
$"producer module for ware '{wareId}'").Id;
|
||||
|
||||
private static string ResolveStorageModuleId(
|
||||
StorageKind storageKind,
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions) =>
|
||||
SelectPreferredModule(
|
||||
moduleDefinitions.Values
|
||||
.OfType<StorageModuleDefinition>()
|
||||
.Where(definition => definition.StorageKind == storageKind),
|
||||
factionId,
|
||||
$"storage module for cargo kind '{storageKind.ToDataValue()}'").Id;
|
||||
|
||||
private static T SelectPreferredModule<T>(
|
||||
IEnumerable<T> candidates,
|
||||
string? factionId,
|
||||
string context)
|
||||
where T : ModuleDefinition
|
||||
{
|
||||
var ordered = candidates
|
||||
.OrderBy(definition => ComputeOwnerRank(definition, factionId))
|
||||
.ThenBy(definition => ComputeModuleRank(definition))
|
||||
.ThenBy(definition => definition.Id, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return ordered.FirstOrDefault()
|
||||
?? throw new InvalidOperationException($"Unable to resolve {context}.");
|
||||
}
|
||||
|
||||
private static int ComputeOwnerRank(ModuleDefinition definition, string? factionId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(factionId))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return definition.Owners.Contains(factionId, StringComparer.Ordinal) ? 0 : 1;
|
||||
}
|
||||
|
||||
private static int ComputeModuleRank(ModuleDefinition definition)
|
||||
{
|
||||
if (definition.ModuleType is ModuleType.DockArea or ModuleType.Storage)
|
||||
{
|
||||
if (definition.Id.Contains("_m_", StringComparison.Ordinal))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (definition.Id.Contains("_s_", StringComparison.Ordinal))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (definition.Id.Contains("_l_", StringComparison.Ordinal))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
@@ -4,114 +4,65 @@ namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class SystemGenerationService
|
||||
{
|
||||
internal List<SolarSystemDefinition> PrepareAuthoredSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
|
||||
authoredSystems
|
||||
private const float KnownSystemSelectionChance = 0.5f;
|
||||
|
||||
internal List<SolarSystemDefinition> PrepareKnownSystems(IReadOnlyList<SolarSystemDefinition> knownSystems) =>
|
||||
knownSystems
|
||||
.Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
|
||||
.ToList();
|
||||
|
||||
internal List<SolarSystemDefinition> ExpandSystems(
|
||||
IReadOnlyList<SolarSystemDefinition> authoredSystems,
|
||||
int targetSystemCount)
|
||||
internal List<SolarSystemDefinition> GenerateSystems(
|
||||
IReadOnlyList<SolarSystemDefinition> knownSystems,
|
||||
WorldGenerationOptions worldGenerationOptions)
|
||||
{
|
||||
var systems = authoredSystems
|
||||
.Select(CloneSystemDefinition)
|
||||
.ToList();
|
||||
|
||||
if (targetSystemCount <= 0)
|
||||
if (worldGenerationOptions.TargetSystemCount <= 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (systems.Count > targetSystemCount)
|
||||
if (knownSystems.Count == 0)
|
||||
{
|
||||
return TrimSystemsToTarget(systems, targetSystemCount);
|
||||
throw new InvalidOperationException("World generation requires at least one known system template.");
|
||||
}
|
||||
|
||||
if (systems.Count >= targetSystemCount || authoredSystems.Count == 0)
|
||||
{
|
||||
return systems;
|
||||
}
|
||||
var systems = new List<SolarSystemDefinition>(worldGenerationOptions.TargetSystemCount);
|
||||
var availableKnownSystems = knownSystems.Select(CloneSystemDefinition).ToList();
|
||||
var templateSystems = knownSystems.Select(CloneSystemDefinition).ToList();
|
||||
|
||||
var existingIds = systems
|
||||
.Select(system => system.Id)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var generatedPositions = BuildGalaxyPositions(
|
||||
authoredSystems.Select(system => ToVector(system.Position)).ToList(),
|
||||
targetSystemCount - systems.Count);
|
||||
var existingIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
var occupiedPositions = new List<Vector3>();
|
||||
var generatedSystemCount = 0;
|
||||
|
||||
for (var index = systems.Count; index < targetSystemCount; index += 1)
|
||||
for (var slotIndex = 0; slotIndex < worldGenerationOptions.TargetSystemCount; slotIndex += 1)
|
||||
{
|
||||
var template = authoredSystems[index % authoredSystems.Count];
|
||||
var name = GeneratedSystemNames[(index - authoredSystems.Count) % GeneratedSystemNames.Length];
|
||||
var id = BuildGeneratedSystemId(name, index + 1);
|
||||
if (ShouldUseKnownSystem(worldGenerationOptions, slotIndex, availableKnownSystems.Count))
|
||||
{
|
||||
var knownSystemIndex = SelectKnownSystemIndex(worldGenerationOptions.Seed, slotIndex, availableKnownSystems.Count);
|
||||
var knownSystem = availableKnownSystems[knownSystemIndex];
|
||||
availableKnownSystems.RemoveAt(knownSystemIndex);
|
||||
systems.Add(knownSystem);
|
||||
existingIds.Add(knownSystem.Id);
|
||||
occupiedPositions.Add(ToVector(knownSystem.Position));
|
||||
continue;
|
||||
}
|
||||
|
||||
var template = templateSystems[generatedSystemCount % templateSystems.Count];
|
||||
var name = GeneratedSystemNames[generatedSystemCount % GeneratedSystemNames.Length];
|
||||
var id = BuildGeneratedSystemId(name, generatedSystemCount + 1);
|
||||
while (!existingIds.Add(id))
|
||||
{
|
||||
id = $"{id}-x";
|
||||
}
|
||||
|
||||
systems.Add(CreateGeneratedSystem(template, name, id, index - authoredSystems.Count, generatedPositions[index - authoredSystems.Count]));
|
||||
var position = BuildGeneratedSystemPosition(occupiedPositions, generatedSystemCount);
|
||||
systems.Add(CreateGeneratedSystem(template, name, id, generatedSystemCount, position));
|
||||
occupiedPositions.Add(position);
|
||||
generatedSystemCount += 1;
|
||||
}
|
||||
|
||||
return systems;
|
||||
}
|
||||
|
||||
private static List<SolarSystemDefinition> TrimSystemsToTarget(IReadOnlyList<SolarSystemDefinition> systems, int targetSystemCount)
|
||||
{
|
||||
var selected = new List<SolarSystemDefinition>(targetSystemCount);
|
||||
|
||||
void AddById(string systemId)
|
||||
{
|
||||
var system = systems.FirstOrDefault(candidate => string.Equals(candidate.Id, systemId, StringComparison.Ordinal));
|
||||
if (system is not null && selected.All(candidate => !string.Equals(candidate.Id, systemId, StringComparison.Ordinal)))
|
||||
{
|
||||
selected.Add(system);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var preferredSystemId in SystemSelectionPolicy.PreferredSystemIds)
|
||||
{
|
||||
AddById(preferredSystemId);
|
||||
}
|
||||
|
||||
foreach (var system in systems)
|
||||
{
|
||||
if (selected.Count >= targetSystemCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (selected.Any(candidate => string.Equals(candidate.Id, system.Id, StringComparison.Ordinal)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
selected.Add(system);
|
||||
}
|
||||
|
||||
if (selected.Count > 0 && selected.Count <= 4)
|
||||
{
|
||||
ApplyCompactGalaxyLayout(selected);
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static void ApplyCompactGalaxyLayout(IReadOnlyList<SolarSystemDefinition> systems)
|
||||
{
|
||||
var compactPositions = new[]
|
||||
{
|
||||
new[] { 0f, 0f, 0f },
|
||||
new[] { 2.6f, 0.02f, -0.42f },
|
||||
new[] { -2.4f, -0.04f, 0.56f },
|
||||
new[] { 0.52f, 0.04f, 2.48f },
|
||||
};
|
||||
|
||||
for (var index = 0; index < systems.Count && index < compactPositions.Length; index += 1)
|
||||
{
|
||||
systems[index].Position = compactPositions[index];
|
||||
}
|
||||
}
|
||||
|
||||
private static SolarSystemDefinition CreateGeneratedSystem(
|
||||
SolarSystemDefinition template,
|
||||
string label,
|
||||
@@ -260,30 +211,38 @@ public sealed class SystemGenerationService
|
||||
return system;
|
||||
}
|
||||
|
||||
private static List<Vector3> BuildGalaxyPositions(IReadOnlyCollection<Vector3> occupiedPositions, int count)
|
||||
private static Vector3 BuildGeneratedSystemPosition(IReadOnlyCollection<Vector3> occupiedPositions, int generatedIndex)
|
||||
{
|
||||
var allPositions = occupiedPositions.ToList();
|
||||
var generated = new List<Vector3>(count);
|
||||
|
||||
for (var index = 0; index < count; index += 1)
|
||||
for (var attempt = 0; attempt < 64; attempt += 1)
|
||||
{
|
||||
Vector3? accepted = null;
|
||||
for (var attempt = 0; attempt < 64; attempt += 1)
|
||||
var candidate = ComputeGeneratedSystemPosition(generatedIndex, attempt);
|
||||
if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
|
||||
{
|
||||
var candidate = ComputeGeneratedSystemPosition(index, attempt);
|
||||
if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
|
||||
{
|
||||
accepted = candidate;
|
||||
break;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
accepted ??= ComputeFallbackGeneratedSystemPosition(index);
|
||||
generated.Add(accepted.Value);
|
||||
allPositions.Add(accepted.Value);
|
||||
}
|
||||
|
||||
return generated;
|
||||
return ComputeFallbackGeneratedSystemPosition(generatedIndex);
|
||||
}
|
||||
|
||||
private static bool ShouldUseKnownSystem(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
int slotIndex,
|
||||
int remainingKnownSystemCount)
|
||||
{
|
||||
if (!worldGenerationOptions.UseKnownSystems || remainingKnownSystemCount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Hash01(worldGenerationOptions.Seed, 700 + slotIndex) >= KnownSystemSelectionChance;
|
||||
}
|
||||
|
||||
private static int SelectKnownSystemIndex(int seed, int slotIndex, int remainingKnownSystemCount)
|
||||
{
|
||||
var selection = Hash01(seed, 900 + slotIndex);
|
||||
return Math.Min((int)(selection * remainingKnownSystemCount), remainingKnownSystemCount - 1);
|
||||
}
|
||||
|
||||
private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
internal static class SystemSelectionPolicy
|
||||
{
|
||||
internal static readonly string[] PreferredSystemIds =
|
||||
[
|
||||
"sol",
|
||||
"helios",
|
||||
];
|
||||
|
||||
internal static string SelectFallbackSystemId(IReadOnlyList<string> availableSystemIds)
|
||||
{
|
||||
foreach (var preferredSystemId in PreferredSystemIds)
|
||||
{
|
||||
if (availableSystemIds.Contains(preferredSystemId, StringComparer.Ordinal))
|
||||
{
|
||||
return preferredSystemId;
|
||||
}
|
||||
}
|
||||
|
||||
return availableSystemIds.FirstOrDefault() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
7
apps/backend/Universe/Scenario/WorldBuildTopology.cs
Normal file
7
apps/backend/Universe/Scenario/WorldBuildTopology.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed record WorldBuildTopology(
|
||||
IReadOnlyList<SolarSystemDefinition> Systems,
|
||||
IReadOnlyList<SystemRuntime> SystemRuntimes,
|
||||
IReadOnlyDictionary<string, SystemRuntime> SystemsById,
|
||||
ScenarioSpatialLayout SpatialLayout);
|
||||
@@ -1,383 +1,26 @@
|
||||
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class WorldBuilder(
|
||||
StaticDataCatalog staticData,
|
||||
IOptions<BalanceOptions> balance,
|
||||
SystemGenerationService generationService,
|
||||
SpatialBuilder spatialBuilder,
|
||||
WorldSeedingService seedingService)
|
||||
WorldTopologyBuilder topologyBuilder,
|
||||
ScenarioValidationService scenarioValidationService,
|
||||
ScenarioContentBuilder contentBuilder,
|
||||
WorldRuntimeAssembler runtimeAssembler)
|
||||
{
|
||||
public SimulationWorld Build(
|
||||
GameStartOptionsDefinition gameStartOptions,
|
||||
public SimulationWorld BuildFromGeneration(WorldGenerationOptions worldGenerationOptions) =>
|
||||
BuildWorld(worldGenerationOptions, null);
|
||||
|
||||
public SimulationWorld BuildFromScenario(ScenarioDefinition scenarioDefinition) =>
|
||||
BuildWorld(scenarioDefinition.WorldGeneration, scenarioDefinition);
|
||||
|
||||
private SimulationWorld BuildWorld(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
ScenarioDefinition? scenarioDefinition)
|
||||
{
|
||||
var systems = generationService.ExpandSystems(
|
||||
generationService.PrepareAuthoredSystems(authoredSystems),
|
||||
gameStartOptions.WorldGeneration.TargetSystemCount);
|
||||
var topology = topologyBuilder.Build(worldGenerationOptions);
|
||||
var scenario = scenarioDefinition ?? scenarioValidationService.CreateEmptyScenario(worldGenerationOptions, topology.Systems);
|
||||
scenarioValidationService.Validate(scenario, topology.Systems.Select(system => system.Id).ToHashSet(StringComparer.Ordinal));
|
||||
|
||||
var scenario = NormalizeScenarioToAvailableSystems(
|
||||
scenarioDefinition,
|
||||
systems.Select(system => system.Id).ToList());
|
||||
|
||||
var systemRuntimes = systems
|
||||
.Select(definition => new SystemRuntime
|
||||
{
|
||||
Definition = definition,
|
||||
Position = ToVector(definition.Position),
|
||||
})
|
||||
.ToList();
|
||||
var systemsById = systemRuntimes.ToDictionary(system => system.Definition.Id, StringComparer.Ordinal);
|
||||
var spatialLayout = spatialBuilder.BuildLayout(systemRuntimes, balance.Value);
|
||||
|
||||
var stations = CreateStations(
|
||||
scenario,
|
||||
systemsById,
|
||||
spatialLayout.SystemGraphs,
|
||||
spatialLayout.Celestials,
|
||||
staticData.ModuleDefinitions,
|
||||
staticData.ItemDefinitions);
|
||||
|
||||
seedingService.InitializeStationStockpiles(stations, staticData.ModuleDefinitions);
|
||||
var refinery = seedingService.SelectRefineryStation(stations, scenario);
|
||||
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
|
||||
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, staticData.ShipDefinitions, patrolRoutes, stations, refinery);
|
||||
|
||||
if (gameStartOptions.WorldGeneration.AiControllerFactionCount < int.MaxValue)
|
||||
{
|
||||
var aiFactionIds = stations
|
||||
.Select(s => s.FactionId)
|
||||
.Concat(ships.Select(s => s.FactionId))
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id) && !string.Equals(id, DefaultFactionId, StringComparison.Ordinal))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.Take(gameStartOptions.WorldGeneration.AiControllerFactionCount)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
aiFactionIds.Add(DefaultFactionId);
|
||||
stations = stations.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
||||
ships = ships.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
||||
}
|
||||
|
||||
var factions = seedingService.CreateFactions(stations, ships);
|
||||
seedingService.BootstrapFactionEconomy(factions, stations);
|
||||
var policies = seedingService.CreatePolicies(factions);
|
||||
var commanders = seedingService.CreateCommanders(factions, stations, ships);
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var playerFaction = gameStartOptions.WorldGeneration.GeneratePlayerFaction
|
||||
? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc)
|
||||
: null;
|
||||
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
|
||||
var world = new SimulationWorld
|
||||
{
|
||||
Label = "Split Viewer / Simulation World",
|
||||
Seed = gameStartOptions.Seed,
|
||||
Systems = systemRuntimes,
|
||||
Celestials = spatialLayout.Celestials,
|
||||
Nodes = spatialLayout.Nodes,
|
||||
Wrecks = [],
|
||||
Stations = stations,
|
||||
Ships = ships,
|
||||
Factions = factions,
|
||||
PlayerFaction = playerFaction,
|
||||
Geopolitics = null,
|
||||
Commanders = commanders,
|
||||
Claims = claims,
|
||||
ConstructionSites = [],
|
||||
MarketOrders = [],
|
||||
Policies = policies,
|
||||
ShipDefinitions = new Dictionary<string, ShipDefinition>(staticData.ShipDefinitions, StringComparer.Ordinal),
|
||||
ItemDefinitions = new Dictionary<string, ItemDefinition>(staticData.ItemDefinitions, StringComparer.Ordinal),
|
||||
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(staticData.ModuleDefinitions, StringComparer.Ordinal),
|
||||
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(staticData.ModuleRecipes, StringComparer.Ordinal),
|
||||
Recipes = new Dictionary<string, RecipeDefinition>(staticData.Recipes, StringComparer.Ordinal),
|
||||
ProductionGraph = staticData.ProductionGraph,
|
||||
OrbitalTimeSeconds = gameStartOptions.Seed * 97d,
|
||||
GeneratedAtUtc = nowUtc,
|
||||
};
|
||||
|
||||
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(world);
|
||||
world.ConstructionSites.AddRange(constructionSites);
|
||||
world.MarketOrders.AddRange(marketOrders);
|
||||
|
||||
var geopolitics = new GeopoliticalSimulationService();
|
||||
geopolitics.Update(world, 0f, []);
|
||||
return world;
|
||||
}
|
||||
|
||||
private static ScenarioDefinition NormalizeScenarioToAvailableSystems(
|
||||
ScenarioDefinition? scenario,
|
||||
IReadOnlyList<string> availableSystemIds)
|
||||
{
|
||||
var fallbackSystemId = SystemSelectionPolicy.SelectFallbackSystemId(availableSystemIds);
|
||||
|
||||
if (scenario is null)
|
||||
{
|
||||
return new ScenarioDefinition
|
||||
{
|
||||
GameStartOptions = new GameStartOptionsDefinition(),
|
||||
InitialStations = [],
|
||||
ShipFormations = [],
|
||||
PatrolRoutes = [],
|
||||
MiningDefaults = new MiningDefaultsDefinition
|
||||
{
|
||||
NodeSystemId = fallbackSystemId,
|
||||
RefinerySystemId = fallbackSystemId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (availableSystemIds.Count == 0)
|
||||
{
|
||||
return scenario;
|
||||
}
|
||||
|
||||
string ResolveSystemId(string systemId) =>
|
||||
availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId;
|
||||
|
||||
return new ScenarioDefinition
|
||||
{
|
||||
GameStartOptions = scenario.GameStartOptions,
|
||||
InitialStations = scenario.InitialStations
|
||||
.Select(station => new InitialStationDefinition
|
||||
{
|
||||
SystemId = ResolveSystemId(station.SystemId),
|
||||
Label = station.Label,
|
||||
Color = station.Color,
|
||||
Objective = station.Objective,
|
||||
StartingModules = station.StartingModules.ToList(),
|
||||
FactionId = station.FactionId,
|
||||
PlanetIndex = station.PlanetIndex,
|
||||
LagrangeSide = station.LagrangeSide,
|
||||
Position = station.Position?.ToArray(),
|
||||
})
|
||||
.ToList(),
|
||||
ShipFormations = scenario.ShipFormations
|
||||
.Select(formation => new ShipFormationDefinition
|
||||
{
|
||||
ShipId = formation.ShipId,
|
||||
Count = formation.Count,
|
||||
Center = formation.Center.ToArray(),
|
||||
SystemId = ResolveSystemId(formation.SystemId),
|
||||
FactionId = formation.FactionId,
|
||||
StartingInventory = new Dictionary<string, float>(formation.StartingInventory, StringComparer.Ordinal),
|
||||
})
|
||||
.ToList(),
|
||||
PatrolRoutes = scenario.PatrolRoutes
|
||||
.Select(route => new PatrolRouteDefinition
|
||||
{
|
||||
SystemId = ResolveSystemId(route.SystemId),
|
||||
Points = route.Points.Select(point => point.ToArray()).ToList(),
|
||||
})
|
||||
.ToList(),
|
||||
MiningDefaults = new MiningDefaultsDefinition
|
||||
{
|
||||
NodeSystemId = ResolveSystemId(scenario.MiningDefaults.NodeSystemId),
|
||||
RefinerySystemId = ResolveSystemId(scenario.MiningDefaults.RefinerySystemId),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static List<StationRuntime> CreateStations(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
var stations = new List<StationRuntime>();
|
||||
var stationIdCounter = 0;
|
||||
|
||||
foreach (var plan in scenario.InitialStations)
|
||||
{
|
||||
if (!systemsById.TryGetValue(plan.SystemId, out var system))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var placement = SpatialBuilder.ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], celestials);
|
||||
var station = new StationRuntime
|
||||
{
|
||||
Id = $"station-{++stationIdCounter}",
|
||||
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);
|
||||
placement.AnchorCelestial.OccupyingStructureId = station.Id;
|
||||
|
||||
var startingModules = BuildStartingModules(plan, moduleDefinitions, itemDefinitions);
|
||||
|
||||
foreach (var moduleId in startingModules)
|
||||
{
|
||||
AddStationModule(station, moduleDefinitions, moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
return stations;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildStartingModules(
|
||||
InitialStationDefinition plan,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
var startingModules = new List<string>(plan.StartingModules.Count > 0
|
||||
? plan.StartingModules
|
||||
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_container_m_01"]);
|
||||
|
||||
EnsureStartingModule(startingModules, "module_arg_dock_m_01_lowtech");
|
||||
|
||||
var objectiveModuleId = GetObjectiveStartingModuleId(plan.Objective);
|
||||
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
|
||||
{
|
||||
EnsureStartingModule(startingModules, objectiveModuleId);
|
||||
|
||||
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
|
||||
{
|
||||
EnsureStartingModule(startingModules, "module_gen_prod_energycells_01");
|
||||
}
|
||||
|
||||
foreach (var storageModuleId in GetRequiredStartingStorageModules(objectiveModuleId, moduleDefinitions, itemDefinitions))
|
||||
{
|
||||
EnsureStartingModule(startingModules, storageModuleId);
|
||||
}
|
||||
}
|
||||
|
||||
return startingModules;
|
||||
}
|
||||
|
||||
private static string? GetObjectiveStartingModuleId(string? objective) =>
|
||||
StationSimulationService.NormalizeStationObjective(objective) switch
|
||||
{
|
||||
"power" => "module_gen_prod_energycells_01",
|
||||
"refinery" => "module_gen_ref_ore_01",
|
||||
"graphene" => "module_gen_prod_graphene_01",
|
||||
"siliconwafers" => "module_gen_prod_siliconwafers_01",
|
||||
"hullparts" => "module_gen_prod_hullparts_01",
|
||||
"claytronics" => "module_gen_prod_claytronics_01",
|
||||
"quantumtubes" => "module_gen_prod_quantumtubes_01",
|
||||
"antimattercells" => "module_gen_prod_antimattercells_01",
|
||||
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
|
||||
"water" => "module_gen_prod_water_01",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static IEnumerable<string> GetRequiredStartingStorageModules(
|
||||
string moduleId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var wareId in moduleDefinition.BuildRecipes
|
||||
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||
.Concat(moduleDefinition.ProductItemIds)
|
||||
.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(moduleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
|
||||
{
|
||||
yield return storageModuleId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureStartingModule(List<string> modules, string moduleId)
|
||||
{
|
||||
if (!modules.Contains(moduleId, StringComparer.Ordinal))
|
||||
{
|
||||
modules.Add(moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById)
|
||||
{
|
||||
return scenario.PatrolRoutes
|
||||
.GroupBy(route => route.SystemId, StringComparer.Ordinal)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group
|
||||
.SelectMany(route => route.Points)
|
||||
.Select(point => NormalizeScenarioPoint(systemsById[group.Key], point))
|
||||
.ToList(),
|
||||
StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private List<ShipRuntime> CreateShips(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
IReadOnlyDictionary<string, ShipDefinition> shipDefinitions,
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
StationRuntime? refinery)
|
||||
{
|
||||
var ships = new List<ShipRuntime>();
|
||||
var shipIdCounter = 0;
|
||||
|
||||
foreach (var formation in scenario.ShipFormations)
|
||||
{
|
||||
if (!shipDefinitions.TryGetValue(formation.ShipId, out var definition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var index = 0; index < formation.Count; index += 1)
|
||||
{
|
||||
var offset = new Vector3((index % 3) * 18f, balance.Value.YPlane, (index / 3) * 18f);
|
||||
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
|
||||
|
||||
ships.Add(new ShipRuntime
|
||||
{
|
||||
Id = $"ship-{++shipIdCounter}",
|
||||
SystemId = formation.SystemId,
|
||||
Definition = definition,
|
||||
FactionId = formation.FactionId ?? DefaultFactionId,
|
||||
Position = position,
|
||||
TargetPosition = position,
|
||||
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials),
|
||||
DefaultBehavior = WorldSeedingService.CreateBehavior(
|
||||
definition,
|
||||
formation.SystemId,
|
||||
formation.FactionId ?? DefaultFactionId,
|
||||
scenario,
|
||||
patrolRoutes,
|
||||
stations,
|
||||
refinery),
|
||||
Skills = WorldSeedingService.CreateSkills(definition),
|
||||
Health = definition.MaxHealth,
|
||||
});
|
||||
|
||||
foreach (var (itemId, amount) in formation.StartingInventory)
|
||||
{
|
||||
if (amount > 0f)
|
||||
{
|
||||
ships[^1].Inventory[itemId] = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ships;
|
||||
var content = contentBuilder.Build(scenario, topology);
|
||||
return runtimeAssembler.Assemble(worldGenerationOptions, topology, content);
|
||||
}
|
||||
}
|
||||
|
||||
62
apps/backend/Universe/Scenario/WorldRuntimeAssembler.cs
Normal file
62
apps/backend/Universe/Scenario/WorldRuntimeAssembler.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class WorldRuntimeAssembler(
|
||||
IStaticDataProvider staticData,
|
||||
WorldSeedingService seedingService)
|
||||
{
|
||||
public SimulationWorld Assemble(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
WorldBuildTopology topology,
|
||||
ScenarioWorldContent content)
|
||||
{
|
||||
seedingService.InitializeStationStockpiles(content.Stations, staticData.ModuleDefinitions);
|
||||
|
||||
var factions = seedingService.CreateFactions(content.Stations, content.Ships);
|
||||
seedingService.BootstrapFactionEconomy(factions, content.Stations);
|
||||
var policies = seedingService.CreatePolicies(factions);
|
||||
var commanders = seedingService.CreateCommanders(factions, content.Stations, content.Ships);
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var playerFaction = worldGenerationOptions.GeneratePlayerFaction
|
||||
? seedingService.CreatePlayerFaction(factions, content.Stations, content.Ships, commanders, policies, nowUtc)
|
||||
: null;
|
||||
var claims = seedingService.CreateClaims(content.Stations, topology.SpatialLayout.Celestials, nowUtc);
|
||||
|
||||
var world = new SimulationWorld
|
||||
{
|
||||
Label = "Split Viewer / Simulation World",
|
||||
Seed = worldGenerationOptions.Seed,
|
||||
Systems = topology.SystemRuntimes.ToList(),
|
||||
Celestials = topology.SpatialLayout.Celestials,
|
||||
Nodes = topology.SpatialLayout.Nodes,
|
||||
Wrecks = [],
|
||||
Stations = content.Stations.ToList(),
|
||||
Ships = content.Ships.ToList(),
|
||||
Factions = factions,
|
||||
PlayerFaction = playerFaction,
|
||||
Geopolitics = null,
|
||||
Commanders = commanders,
|
||||
Claims = claims,
|
||||
ConstructionSites = [],
|
||||
MarketOrders = [],
|
||||
Policies = policies,
|
||||
ShipDefinitions = new Dictionary<string, ShipDefinition>(staticData.ShipDefinitions, StringComparer.Ordinal),
|
||||
ItemDefinitions = new Dictionary<string, ItemDefinition>(staticData.ItemDefinitions, StringComparer.Ordinal),
|
||||
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(staticData.ModuleDefinitions, StringComparer.Ordinal),
|
||||
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(staticData.ModuleRecipes, StringComparer.Ordinal),
|
||||
Recipes = new Dictionary<string, RecipeDefinition>(staticData.Recipes, StringComparer.Ordinal),
|
||||
ProductionGraph = staticData.ProductionGraph,
|
||||
OrbitalTimeSeconds = worldGenerationOptions.Seed * 97d,
|
||||
GeneratedAtUtc = nowUtc,
|
||||
};
|
||||
|
||||
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(world);
|
||||
world.ConstructionSites.AddRange(constructionSites);
|
||||
world.MarketOrders.AddRange(marketOrders);
|
||||
|
||||
var geopolitics = new GeopoliticalSimulationService();
|
||||
geopolitics.Update(world, 0f, []);
|
||||
return world;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class WorldSeedingService
|
||||
public sealed class WorldSeedingService(IStaticDataProvider staticData)
|
||||
{
|
||||
internal List<FactionRuntime> CreateFactions(
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
@@ -18,7 +19,7 @@ public sealed class WorldSeedingService
|
||||
|
||||
if (factionIds.Count == 0)
|
||||
{
|
||||
factionIds.Add(DefaultFactionId);
|
||||
return [];
|
||||
}
|
||||
|
||||
return factionIds.Select(CreateFaction).ToList();
|
||||
@@ -70,15 +71,6 @@ public sealed class WorldSeedingService
|
||||
}
|
||||
}
|
||||
|
||||
internal StationRuntime? SelectRefineryStation(IReadOnlyCollection<StationRuntime> stations, ScenarioDefinition scenario)
|
||||
{
|
||||
return stations.FirstOrDefault(station =>
|
||||
string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal) &&
|
||||
station.SystemId == scenario.MiningDefaults.RefinerySystemId)
|
||||
?? stations.FirstOrDefault(station =>
|
||||
string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
internal List<ClaimRuntime> CreateClaims(
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
@@ -183,39 +175,32 @@ public sealed class WorldSeedingService
|
||||
private static bool HasSatisfiedStarterObjectiveLayout(SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
var role = StationSimulationService.DetermineStationRole(station);
|
||||
var objectiveModuleId = role switch
|
||||
{
|
||||
"power" => "module_gen_prod_energycells_01",
|
||||
"refinery" => "module_gen_prod_refinedmetals_01",
|
||||
"graphene" => "module_gen_prod_graphene_01",
|
||||
"siliconwafers" => "module_gen_prod_siliconwafers_01",
|
||||
"hullparts" => "module_gen_prod_hullparts_01",
|
||||
"claytronics" => "module_gen_prod_claytronics_01",
|
||||
"quantumtubes" => "module_gen_prod_quantumtubes_01",
|
||||
"antimattercells" => "module_gen_prod_antimattercells_01",
|
||||
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
|
||||
"water" => "module_gen_prod_water_01",
|
||||
_ => null,
|
||||
};
|
||||
var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(role, station.FactionId, world.ModuleDefinitions);
|
||||
|
||||
if (objectiveModuleId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!station.InstalledModules.Contains("module_arg_dock_m_01_lowtech", StringComparer.Ordinal)
|
||||
var requiredDockModuleId = StarterStationLayoutResolver.ResolveDockModuleId(station.FactionId, world.ModuleDefinitions);
|
||||
if (!station.InstalledModules.Contains(requiredDockModuleId, StringComparer.Ordinal)
|
||||
|| !station.InstalledModules.Contains(objectiveModuleId, StringComparer.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)
|
||||
&& !station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
|
||||
var powerModuleId = StarterStationLayoutResolver.ResolvePowerModuleId(station.FactionId, world.ModuleDefinitions);
|
||||
if (!string.Equals(objectiveModuleId, powerModuleId, StringComparison.Ordinal)
|
||||
&& !station.InstalledModules.Contains(powerModuleId, StringComparer.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var storageModuleId in GetRequiredStorageModulesForInstalledObjective(world, objectiveModuleId))
|
||||
foreach (var storageModuleId in StarterStationLayoutResolver.ResolveRequiredStorageModuleIds(
|
||||
objectiveModuleId,
|
||||
station.FactionId,
|
||||
world.ModuleDefinitions,
|
||||
world.ItemDefinitions))
|
||||
{
|
||||
if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal))
|
||||
{
|
||||
@@ -226,30 +211,6 @@ public sealed class WorldSeedingService
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetRequiredStorageModulesForInstalledObjective(SimulationWorld world, string moduleId)
|
||||
{
|
||||
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var wareId in moduleDefinition.BuildRecipes
|
||||
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||
.Concat(moduleDefinition.ProductItemIds)
|
||||
.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
if (!world.ItemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
|
||||
{
|
||||
yield return storageModuleId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
|
||||
{
|
||||
var policies = new List<PolicySetRuntime>(factions.Count);
|
||||
@@ -355,8 +316,8 @@ public sealed class WorldSeedingService
|
||||
IReadOnlyCollection<PolicySetRuntime> policies,
|
||||
DateTimeOffset nowUtc)
|
||||
{
|
||||
var sovereignFaction = factions.FirstOrDefault(faction => string.Equals(faction.Id, DefaultFactionId, StringComparison.Ordinal))
|
||||
?? factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).First();
|
||||
var sovereignFaction = factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).FirstOrDefault()
|
||||
?? throw new InvalidOperationException("Cannot create a player faction without at least one faction in the world.");
|
||||
|
||||
var player = new PlayerFactionRuntime
|
||||
{
|
||||
@@ -434,122 +395,55 @@ public sealed class WorldSeedingService
|
||||
return player;
|
||||
}
|
||||
|
||||
internal static DefaultBehaviorRuntime CreateBehavior(
|
||||
ShipDefinition definition,
|
||||
string systemId,
|
||||
string factionId,
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
StationRuntime? refinery)
|
||||
private FactionRuntime CreateFaction(string factionId)
|
||||
{
|
||||
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)
|
||||
if (!staticData.FactionDefinitions.TryGetValue(factionId, out var definition))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "construct-station",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
PreferredConstructionSiteId = null,
|
||||
};
|
||||
throw new InvalidOperationException($"Faction '{factionId}' is not defined in static data.");
|
||||
}
|
||||
|
||||
if (HasCapabilities(definition, "mining") && homeStation is not null)
|
||||
return new FactionRuntime
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
AreaSystemId = scenario.MiningDefaults.NodeSystemId,
|
||||
MaxSystemRange = definition.CargoCapacity >= 120f ? 3 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "advanced-auto-trade",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
MaxSystemRange = 2,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && patrolRoutes.TryGetValue(systemId, out var route))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "patrol",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
AreaSystemId = systemId,
|
||||
PatrolPoints = route,
|
||||
PatrolIndex = 0,
|
||||
};
|
||||
}
|
||||
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "idle",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
Id = definition.Id,
|
||||
Label = definition.Label,
|
||||
Color = ResolveFactionColor(definition),
|
||||
Credits = MinimumFactionCredits,
|
||||
};
|
||||
}
|
||||
|
||||
internal static ShipSkillProfileRuntime CreateSkills(ShipDefinition definition)
|
||||
{
|
||||
return definition.Kind switch
|
||||
{
|
||||
"transport" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 4, Mining = 1, Combat = 1, Construction = 1 },
|
||||
"construction" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 1, Combat = 1, Construction = 4 },
|
||||
"military" => new ShipSkillProfileRuntime { Navigation = 4, Trade = 1, Mining = 1, Combat = 4, Construction = 1 },
|
||||
_ when HasCapabilities(definition, "mining") => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 4, Combat = 1, Construction = 1 },
|
||||
_ => new ShipSkillProfileRuntime { Navigation = 3, Trade = 2, Mining = 1, Combat = 1, Construction = 1 },
|
||||
};
|
||||
}
|
||||
|
||||
private static FactionRuntime CreateFaction(string factionId)
|
||||
{
|
||||
return factionId switch
|
||||
{
|
||||
DefaultFactionId => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
Label = "Sol Dominion",
|
||||
Color = "#7ed4ff",
|
||||
Credits = MinimumFactionCredits,
|
||||
},
|
||||
"asterion-league" => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
Label = "Asterion League",
|
||||
Color = "#ff8f70",
|
||||
Credits = MinimumFactionCredits,
|
||||
},
|
||||
"nadir-syndicate" => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
Label = "Nadir Syndicate",
|
||||
Color = "#91e6a8",
|
||||
Credits = MinimumFactionCredits,
|
||||
},
|
||||
_ => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
Label = ToFactionLabel(factionId),
|
||||
Color = "#c7d2e0",
|
||||
Credits = MinimumFactionCredits,
|
||||
},
|
||||
};
|
||||
}
|
||||
private static string ResolveFactionColor(FactionDefinition definition) =>
|
||||
definition.Id switch
|
||||
{
|
||||
"alliance" => "#c084fc",
|
||||
"antigone" => "#f97316",
|
||||
"argon" => "#3b82f6",
|
||||
"boron" => "#14b8a6",
|
||||
"freesplit" => "#ef4444",
|
||||
"hatikvah" => "#84cc16",
|
||||
"holyorder" => "#d97706",
|
||||
"loanshark" => "#f59e0b",
|
||||
"ministry" => "#a3e635",
|
||||
"paranid" => "#eab308",
|
||||
"pioneers" => "#60a5fa",
|
||||
"scaleplate" => "#94a3b8",
|
||||
"scavenger" => "#64748b",
|
||||
"split" => "#b91c1c",
|
||||
"teladi" => "#22c55e",
|
||||
"terran" => "#38bdf8",
|
||||
"trinity" => "#2dd4bf",
|
||||
"xenon" => "#9ca3af",
|
||||
_ => definition.RaceId switch
|
||||
{
|
||||
"argon" => "#3b82f6",
|
||||
"boron" => "#14b8a6",
|
||||
"paranid" => "#eab308",
|
||||
"split" => "#b91c1c",
|
||||
"teladi" => "#22c55e",
|
||||
"terran" => "#38bdf8",
|
||||
"xenon" => "#9ca3af",
|
||||
_ => "#94a3b8",
|
||||
},
|
||||
};
|
||||
|
||||
private static void InitializeStationPopulation(
|
||||
StationRuntime station,
|
||||
@@ -563,12 +457,4 @@ public sealed class WorldSeedingService
|
||||
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
|
||||
}
|
||||
|
||||
private static string ToFactionLabel(string factionId)
|
||||
{
|
||||
return string.Join(" ",
|
||||
factionId
|
||||
.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(segment => char.ToUpperInvariant(segment[0]) + segment[1..]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
29
apps/backend/Universe/Scenario/WorldTopologyBuilder.cs
Normal file
29
apps/backend/Universe/Scenario/WorldTopologyBuilder.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class WorldTopologyBuilder(
|
||||
IStaticDataProvider staticData,
|
||||
SystemGenerationService generationService,
|
||||
SpatialBuilder spatialBuilder)
|
||||
{
|
||||
public WorldBuildTopology Build(WorldGenerationOptions worldGenerationOptions)
|
||||
{
|
||||
var systems = generationService.GenerateSystems(
|
||||
generationService.PrepareKnownSystems(staticData.KnownSystems),
|
||||
worldGenerationOptions);
|
||||
|
||||
var systemRuntimes = systems
|
||||
.Select(definition => new SystemRuntime
|
||||
{
|
||||
Definition = definition,
|
||||
Position = LoaderSupport.ToVector(definition.Position),
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var systemsById = systemRuntimes.ToDictionary(system => system.Definition.Id, StringComparer.Ordinal);
|
||||
var spatialLayout = spatialBuilder.BuildLayout(systemRuntimes);
|
||||
|
||||
return new WorldBuildTopology(systems, systemRuntimes, systemsById, spatialLayout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user