609 lines
21 KiB
C#
609 lines
21 KiB
C#
using System.Text.Json;
|
|
using SpaceGame.Simulation.Api.Data;
|
|
|
|
namespace SpaceGame.Simulation.Api.Simulation;
|
|
|
|
public sealed partial class ScenarioLoader
|
|
{
|
|
private const string DefaultFactionId = "sol-dominion";
|
|
private const int WorldSeed = 1;
|
|
private const float MinimumFactionCredits = 0f;
|
|
private const float MinimumRefineryOre = 0f;
|
|
private const float MinimumRefineryStock = 0f;
|
|
private const float MinimumShipyardStock = 0f;
|
|
private const float MinimumSystemSeparation = 3.2f;
|
|
private const float LocalSpaceRadius = 10_000f;
|
|
private static readonly string[] GeneratedSystemNames =
|
|
[
|
|
"Aquila Verge",
|
|
"Orion Fold",
|
|
"Draco Span",
|
|
"Lyra Shoal",
|
|
"Cygnus March",
|
|
"Vela Crossing",
|
|
"Carina Wake",
|
|
"Phoenix Rest",
|
|
"Hydra Loom",
|
|
"Cassio Reach",
|
|
"Lupus Chain",
|
|
"Pavo Line",
|
|
"Serpens Rise",
|
|
"Cetus Hollow",
|
|
"Delphin Crown",
|
|
"Volans Drift",
|
|
"Ara Bastion",
|
|
"Indus Veil",
|
|
"Pyxis Trace",
|
|
"Lacerta Bloom",
|
|
"Columba Shroud",
|
|
"Dorado Expanse",
|
|
"Reticulum Run",
|
|
"Norma Edge",
|
|
"Crux Horizon",
|
|
"Sagitta Corridor",
|
|
"Monoceros Deep",
|
|
"Eridan Spur",
|
|
"Centauri Shelf",
|
|
"Antlia Reach",
|
|
"Horologium Gate",
|
|
"Telescopium Strand",
|
|
];
|
|
private static readonly StarProfile[] StarProfiles =
|
|
[
|
|
new("main-sequence", "#ffd27a", "#ffb14a", 696340f),
|
|
new("blue-white", "#9dc6ff", "#66a0ff", 930000f),
|
|
new("white-dwarf", "#f1f5ff", "#b8caff", 12000f),
|
|
new("brown-dwarf", "#b97d56", "#8a5438", 70000f),
|
|
new("neutron-star", "#d9ebff", "#7ab4ff", 18f),
|
|
new("binary-main-sequence", "#ffe09f", "#ffbe6b", 780000f),
|
|
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f),
|
|
];
|
|
private static readonly PlanetProfile[] PlanetProfiles =
|
|
[
|
|
new("barren", "sphere", "#bca48f", 2800f, 0.22f, 0, false),
|
|
new("terrestrial", "sphere", "#58a36c", 6400f, 0.28f, 1, false),
|
|
new("oceanic", "sphere", "#4f84c4", 7000f, 0.30f, 2, false),
|
|
new("desert", "sphere", "#d4a373", 5200f, 0.26f, 0, false),
|
|
new("ice", "sphere", "#c8e4ff", 5800f, 0.32f, 1, false),
|
|
new("gas-giant", "oblate", "#d9b06f", 45000f, 1.40f, 8, true),
|
|
new("ice-giant", "oblate", "#8fc0d8", 25000f, 1.00f, 5, true),
|
|
new("lava", "sphere", "#db6846", 3200f, 0.20f, 0, false),
|
|
];
|
|
|
|
private readonly string _dataRoot;
|
|
private readonly WorldGenerationOptions _worldGeneration;
|
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
};
|
|
|
|
public ScenarioLoader(string contentRootPath, WorldGenerationOptions? worldGeneration = null)
|
|
{
|
|
_dataRoot = Path.GetFullPath(Path.Combine(contentRootPath, "..", "..", "shared", "data"));
|
|
_worldGeneration = worldGeneration ?? new WorldGenerationOptions();
|
|
}
|
|
|
|
public SimulationWorld Load()
|
|
{
|
|
var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json");
|
|
var systems = ExpandSystems(
|
|
InjectSpecialSystems(authoredSystems),
|
|
_worldGeneration.TargetSystemCount);
|
|
var scenario = NormalizeScenarioToAvailableSystems(
|
|
Read<ScenarioDefinition>("scenario.json"),
|
|
systems.Select((system) => system.Id).ToList());
|
|
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
|
|
var ships = Read<List<ShipDefinition>>("ships.json");
|
|
var items = NormalizeItems(Read<List<ItemDefinition>>("items.json"));
|
|
var balance = Read<BalanceDefinition>("balance.json");
|
|
var recipes = BuildRecipes(items, ships, modules);
|
|
var moduleRecipes = BuildModuleRecipes(modules);
|
|
|
|
var moduleDefinitions = modules.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
|
var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
|
var itemDefinitions = items.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
|
var recipeDefinitions = recipes.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
|
var moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal);
|
|
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 systemGraphs = systemRuntimes.ToDictionary(
|
|
(system) => system.Definition.Id,
|
|
(system) => BuildSystemSpatialGraph(system),
|
|
StringComparer.Ordinal);
|
|
|
|
var celestials = new List<CelestialRuntime>();
|
|
var nodes = new List<ResourceNodeRuntime>();
|
|
var nodeIdCounter = 0;
|
|
foreach (var graph in systemGraphs.Values)
|
|
{
|
|
celestials.AddRange(graph.Celestials);
|
|
}
|
|
|
|
foreach (var system in systemRuntimes)
|
|
{
|
|
var systemGraph = systemGraphs[system.Definition.Id];
|
|
foreach (var node in system.Definition.ResourceNodes)
|
|
{
|
|
var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node);
|
|
var resourceNode = new ResourceNodeRuntime
|
|
{
|
|
Id = $"node-{++nodeIdCounter}",
|
|
SystemId = system.Definition.Id,
|
|
Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane),
|
|
SourceKind = node.SourceKind,
|
|
ItemId = node.ItemId,
|
|
CelestialId = anchorCelestial?.Id,
|
|
OrbitRadius = node.RadiusOffset,
|
|
OrbitPhase = node.Angle,
|
|
OrbitInclination = DegreesToRadians(node.InclinationDegrees),
|
|
OreRemaining = node.OreAmount,
|
|
MaxOre = node.OreAmount,
|
|
};
|
|
|
|
nodes.Add(resourceNode);
|
|
}
|
|
}
|
|
|
|
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 = 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,
|
|
Position = placement.Position,
|
|
FactionId = plan.FactionId ?? DefaultFactionId,
|
|
};
|
|
|
|
station.CelestialId = placement.AnchorCelestial.Id;
|
|
stations.Add(station);
|
|
placement.AnchorCelestial.OccupyingStructureId = station.Id;
|
|
|
|
var startingModules = plan.StartingModules.Count > 0
|
|
? plan.StartingModules
|
|
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01", "module_arg_stor_liquid_m_01"];
|
|
foreach (var moduleId in startingModules)
|
|
{
|
|
AddStationModule(stations[^1], moduleDefinitions, moduleId);
|
|
}
|
|
}
|
|
|
|
foreach (var station in stations)
|
|
{
|
|
InitializeStationPopulation(station);
|
|
station.Inventory["refinedmetals"] = 120f;
|
|
if (station.Population > 0f)
|
|
{
|
|
station.Inventory["water"] = MathF.Max(80f, station.Population * 1.5f);
|
|
}
|
|
}
|
|
|
|
var refinery = stations.FirstOrDefault((station) =>
|
|
HasInstalledModules(station, "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01") &&
|
|
station.SystemId == scenario.MiningDefaults.RefinerySystemId)
|
|
?? stations.FirstOrDefault((station) => HasInstalledModules(station, "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01"));
|
|
|
|
var patrolRoutes = 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);
|
|
|
|
var shipsRuntime = 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.YPlane, (index / 3) * 18f);
|
|
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
|
|
shipsRuntime.Add(new ShipRuntime
|
|
{
|
|
Id = $"ship-{++shipIdCounter}",
|
|
SystemId = formation.SystemId,
|
|
Definition = definition,
|
|
FactionId = formation.FactionId ?? DefaultFactionId,
|
|
Position = position,
|
|
TargetPosition = position,
|
|
SpatialState = CreateInitialShipSpatialState(formation.SystemId, position, celestials),
|
|
DefaultBehavior = CreateBehavior(definition, formation.SystemId, scenario, patrolRoutes, refinery),
|
|
ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending },
|
|
Health = definition.MaxHealth,
|
|
});
|
|
}
|
|
}
|
|
|
|
var factions = CreateFactions(stations, shipsRuntime);
|
|
BootstrapFactionEconomy(factions, stations);
|
|
var policies = CreatePolicies(factions);
|
|
var commanders = CreateCommanders(factions, stations, shipsRuntime);
|
|
var now = DateTimeOffset.UtcNow;
|
|
var claims = CreateClaims(stations, celestials, now);
|
|
var (constructionSites, marketOrders) = CreateConstructionSites(stations, claims, moduleRecipeDefinitions);
|
|
|
|
return new SimulationWorld
|
|
{
|
|
Label = "Split Viewer / Simulation World",
|
|
Seed = WorldSeed,
|
|
Balance = balance,
|
|
Systems = systemRuntimes,
|
|
Celestials = celestials,
|
|
Nodes = nodes,
|
|
Stations = stations,
|
|
Ships = shipsRuntime,
|
|
Factions = factions,
|
|
Commanders = commanders,
|
|
Claims = claims,
|
|
ConstructionSites = constructionSites,
|
|
MarketOrders = marketOrders,
|
|
Policies = policies,
|
|
ShipDefinitions = shipDefinitions,
|
|
ItemDefinitions = itemDefinitions,
|
|
ModuleDefinitions = moduleDefinitions,
|
|
ModuleRecipes = moduleRecipeDefinitions,
|
|
Recipes = recipeDefinitions,
|
|
OrbitalTimeSeconds = WorldSeed * 97d,
|
|
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
|
};
|
|
}
|
|
|
|
private T Read<T>(string fileName)
|
|
{
|
|
var path = Path.Combine(_dataRoot, fileName);
|
|
var json = File.ReadAllText(path);
|
|
return JsonSerializer.Deserialize<T>(json, _jsonOptions)
|
|
?? throw new InvalidOperationException($"Unable to read {fileName}.");
|
|
}
|
|
|
|
private static ScenarioDefinition NormalizeScenarioToAvailableSystems(
|
|
ScenarioDefinition scenario,
|
|
IReadOnlyList<string> availableSystemIds)
|
|
{
|
|
if (availableSystemIds.Count == 0)
|
|
{
|
|
return scenario;
|
|
}
|
|
|
|
var fallbackSystemId = availableSystemIds.Contains("sol", StringComparer.Ordinal)
|
|
? "sol"
|
|
: availableSystemIds[0];
|
|
|
|
string ResolveSystemId(string systemId) =>
|
|
availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId;
|
|
|
|
return new ScenarioDefinition
|
|
{
|
|
InitialStations = scenario.InitialStations
|
|
.Select((station) => new InitialStationDefinition
|
|
{
|
|
SystemId = ResolveSystemId(station.SystemId),
|
|
Label = station.Label,
|
|
Color = station.Color,
|
|
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,
|
|
})
|
|
.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 Vector3 ToVector(float[] values) => new(values[0], values[1], values[2]);
|
|
|
|
private static Vector3 NormalizeScenarioPoint(SystemRuntime system, float[] values)
|
|
{
|
|
var raw = ToVector(values);
|
|
var relativeToSystem = new Vector3(
|
|
raw.X - system.Position.X,
|
|
raw.Y - system.Position.Y,
|
|
raw.Z - system.Position.Z);
|
|
|
|
return relativeToSystem.LengthSquared() < raw.LengthSquared()
|
|
? relativeToSystem
|
|
: raw;
|
|
}
|
|
|
|
private static bool HasInstalledModules(StationRuntime station, params string[] modules) =>
|
|
modules.All((moduleId) => station.Modules.Any((candidate) => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal)));
|
|
|
|
private static bool HasCapabilities(ShipDefinition definition, params string[] capabilities) =>
|
|
capabilities.All((cap) => definition.Capabilities.Contains(cap, StringComparer.Ordinal));
|
|
|
|
private static void AddStationModule(StationRuntime station, IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, string moduleId)
|
|
{
|
|
if (!moduleDefinitions.TryGetValue(moduleId, out var definition))
|
|
{
|
|
return;
|
|
}
|
|
|
|
station.Modules.Add(new StationModuleRuntime
|
|
{
|
|
Id = $"{station.Id}-module-{station.Modules.Count + 1}",
|
|
ModuleId = moduleId,
|
|
Health = definition.Hull,
|
|
MaxHealth = definition.Hull,
|
|
});
|
|
station.Radius = GetStationRadius(moduleDefinitions, station);
|
|
}
|
|
|
|
private static float GetStationRadius(IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, StationRuntime station)
|
|
{
|
|
var totalArea = station.Modules
|
|
.Select((module) => moduleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f)
|
|
.Sum();
|
|
return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f)));
|
|
}
|
|
|
|
private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
|
|
|
|
private static int CountModules(IEnumerable<string> modules, string moduleId) =>
|
|
modules.Count((candidate) => string.Equals(candidate, moduleId, StringComparison.Ordinal));
|
|
|
|
private static float ComputeWorkforceRatio(float population, float workforceRequired)
|
|
{
|
|
if (workforceRequired <= 0.01f)
|
|
{
|
|
return 1f;
|
|
}
|
|
|
|
var staffedRatio = MathF.Min(1f, population / workforceRequired);
|
|
return 0.1f + (0.9f * staffedRatio);
|
|
}
|
|
|
|
private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
|
|
modules
|
|
.Where((module) => module.Construction is not null || module.Production.Count > 0)
|
|
.Select((module) => new ModuleRecipeDefinition
|
|
{
|
|
ModuleId = module.Id,
|
|
Duration = module.Construction?.ProductionTime ?? module.Production[0].Time,
|
|
Inputs = (module.Construction?.Requirements ?? module.Production[0].Wares)
|
|
.Select((input) => new RecipeInputDefinition
|
|
{
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
})
|
|
.ToList(),
|
|
})
|
|
.ToList();
|
|
|
|
private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships, IReadOnlyCollection<ModuleDefinition> modules)
|
|
{
|
|
var recipes = new List<RecipeDefinition>();
|
|
var preferredProducerByItemId = modules
|
|
.Where((module) => module.Products.Count > 0)
|
|
.GroupBy((module) => module.Products[0], StringComparer.Ordinal)
|
|
.ToDictionary(
|
|
(group) => group.Key,
|
|
(group) => group.OrderBy((module) => module.Id, StringComparer.Ordinal).First().Id,
|
|
StringComparer.Ordinal);
|
|
|
|
foreach (var item in items)
|
|
{
|
|
if (item.Production.Count > 0)
|
|
{
|
|
foreach (var production in item.Production)
|
|
{
|
|
recipes.Add(new RecipeDefinition
|
|
{
|
|
Id = $"{item.Id}-{production.Method}-production",
|
|
Label = production.Name == "Universal"
|
|
? item.Name
|
|
: $"{item.Name} ({production.Name})",
|
|
FacilityCategory = InferFacilityCategory(item),
|
|
Duration = production.Time,
|
|
Priority = InferRecipePriority(item),
|
|
RequiredModules = InferRequiredModules(item, preferredProducerByItemId),
|
|
Inputs = production.Wares
|
|
.Select((input) => new RecipeInputDefinition
|
|
{
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
})
|
|
.ToList(),
|
|
Outputs =
|
|
[
|
|
new RecipeOutputDefinition
|
|
{
|
|
ItemId = item.Id,
|
|
Amount = production.Amount,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (item.Construction is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
recipes.Add(new RecipeDefinition
|
|
{
|
|
Id = item.Construction.RecipeId ?? $"{item.Id}-production",
|
|
Label = item.Name,
|
|
FacilityCategory = item.Construction.FacilityCategory,
|
|
Duration = item.Construction.CycleTime,
|
|
Priority = item.Construction.Priority,
|
|
RequiredModules = item.Construction.RequiredModules.ToList(),
|
|
Inputs = item.Construction.Requirements
|
|
.Select((input) => new RecipeInputDefinition
|
|
{
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
})
|
|
.ToList(),
|
|
Outputs =
|
|
[
|
|
new RecipeOutputDefinition
|
|
{
|
|
ItemId = item.Id,
|
|
Amount = item.Construction.BatchSize,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
foreach (var ship in ships)
|
|
{
|
|
if (ship.Construction is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
recipes.Add(new RecipeDefinition
|
|
{
|
|
Id = ship.Construction.RecipeId ?? $"{ship.Id}-construction",
|
|
Label = $"{ship.Label} Construction",
|
|
FacilityCategory = ship.Construction.FacilityCategory,
|
|
Duration = ship.Construction.CycleTime,
|
|
Priority = ship.Construction.Priority,
|
|
RequiredModules = ship.Construction.RequiredModules.ToList(),
|
|
Inputs = ship.Construction.Requirements
|
|
.Select((input) => new RecipeInputDefinition
|
|
{
|
|
ItemId = input.ItemId,
|
|
Amount = input.Amount,
|
|
})
|
|
.ToList(),
|
|
ShipOutputId = ship.Id,
|
|
});
|
|
}
|
|
|
|
return recipes;
|
|
}
|
|
|
|
private static string InferFacilityCategory(ItemDefinition item) =>
|
|
item.Group switch
|
|
{
|
|
"agricultural" or "food" or "pharmaceutical" or "water" => "farm",
|
|
_ => "station",
|
|
};
|
|
|
|
private static List<string> InferRequiredModules(ItemDefinition item, IReadOnlyDictionary<string, string> preferredProducerByItemId)
|
|
{
|
|
if (preferredProducerByItemId.TryGetValue(item.Id, out var moduleId))
|
|
{
|
|
return [moduleId];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private static int InferRecipePriority(ItemDefinition item) =>
|
|
item.Group switch
|
|
{
|
|
"energy" => 140,
|
|
"water" => 130,
|
|
"food" => 120,
|
|
"agricultural" => 110,
|
|
"refined" => 100,
|
|
"hightech" => 90,
|
|
"shiptech" => 80,
|
|
"pharmaceutical" => 70,
|
|
_ => 60,
|
|
};
|
|
|
|
private static List<ItemDefinition> NormalizeItems(List<ItemDefinition> items)
|
|
{
|
|
foreach (var item in items)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(item.Type))
|
|
{
|
|
item.Type = string.IsNullOrWhiteSpace(item.Group) ? "material" : item.Group;
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
|
|
{
|
|
foreach (var module in modules)
|
|
{
|
|
if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product))
|
|
{
|
|
module.Products = [module.Product];
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(module.ProductionMode))
|
|
{
|
|
module.ProductionMode = string.Equals(module.Type, "buildmodule", StringComparison.Ordinal)
|
|
? "commanded"
|
|
: "passive";
|
|
}
|
|
|
|
if (module.WorkforceNeeded <= 0f)
|
|
{
|
|
module.WorkforceNeeded = module.WorkForce?.Max ?? 0f;
|
|
}
|
|
}
|
|
|
|
return modules;
|
|
}
|
|
|
|
private static Vector3 Scale(Vector3 vector, float scale) => new(vector.X * scale, vector.Y * scale, vector.Z * scale);
|
|
|
|
private static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f);
|
|
|
|
private static Vector3 NormalizeOrFallback(Vector3 vector, Vector3 fallback)
|
|
{
|
|
var length = MathF.Sqrt(vector.LengthSquared());
|
|
if (length <= 0.0001f)
|
|
{
|
|
return fallback;
|
|
}
|
|
|
|
return vector.Divide(length);
|
|
}
|
|
|
|
}
|