Implement roadmap phases 1 through 8

This commit is contained in:
2026-03-14 02:30:15 -04:00
parent 86b8f4a73f
commit ddca4a16d5
10 changed files with 3862 additions and 198 deletions

View File

@@ -13,6 +13,11 @@ public sealed class ScenarioLoader
private const float MinimumRefineryStock = 0f;
private const float MinimumShipyardStock = 0f;
private const float MinimumSystemSeparation = 3200f;
private const float StarBubbleRadiusPadding = 40f;
private const float PlanetBubbleRadiusPadding = 80f;
private const float MoonBubbleRadiusPadding = 40f;
private const float LagrangeBubbleRadius = 150f;
private const float ResourceBubbleRadius = 120f;
private static readonly string[] GeneratedSystemNames =
[
"Aquila Verge",
@@ -88,12 +93,14 @@ public sealed class ScenarioLoader
var ships = Read<List<ShipDefinition>>("ships.json");
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
var items = Read<List<ItemDefinition>>("items.json");
var recipes = Read<List<RecipeDefinition>>("recipes.json");
var moduleRecipes = Read<List<ModuleRecipeDefinition>>("module-recipes.json");
var balance = Read<BalanceDefinition>("balance.json");
var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
var constructibleDefinitions = constructibles.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
@@ -103,14 +110,26 @@ public sealed class ScenarioLoader
})
.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 nodes = new List<ResourceNodeRuntime>();
var spatialNodes = new List<NodeRuntime>();
var localBubbles = new List<LocalBubbleRuntime>();
var nodeIdCounter = 0;
foreach (var graph in systemGraphs.Values)
{
spatialNodes.AddRange(graph.Nodes);
localBubbles.AddRange(graph.Bubbles);
}
foreach (var system in systemRuntimes)
{
foreach (var node in system.Definition.ResourceNodes)
{
nodes.Add(new ResourceNodeRuntime
var resourceNode = new ResourceNodeRuntime
{
Id = $"node-{++nodeIdCounter}",
SystemId = system.Definition.Id,
@@ -122,6 +141,24 @@ public sealed class ScenarioLoader
ItemId = node.ItemId,
OreRemaining = node.OreAmount,
MaxOre = node.OreAmount,
};
nodes.Add(resourceNode);
var bubbleId = $"bubble-{resourceNode.Id}";
spatialNodes.Add(new NodeRuntime
{
Id = resourceNode.Id,
SystemId = resourceNode.SystemId,
Kind = "resource-site",
Position = resourceNode.Position,
BubbleId = bubbleId,
});
localBubbles.Add(new LocalBubbleRuntime
{
Id = bubbleId,
NodeId = resourceNode.Id,
SystemId = resourceNode.SystemId,
Radius = ResourceBubbleRadius,
});
}
}
@@ -135,14 +172,41 @@ public sealed class ScenarioLoader
continue;
}
stations.Add(new StationRuntime
var placement = ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], spatialNodes);
var station = new StationRuntime
{
Id = $"station-{++stationIdCounter}",
SystemId = system.Definition.Id,
Definition = definition,
Position = ResolveStationPosition(system, plan, balance),
Position = placement.Position,
FactionId = plan.FactionId ?? DefaultFactionId,
};
var stationNodeId = $"node-{station.Id}";
var stationBubbleId = $"bubble-{station.Id}";
station.NodeId = stationNodeId;
station.BubbleId = stationBubbleId;
station.AnchorNodeId = placement.AnchorNode.Id;
stations.Add(station);
spatialNodes.Add(new NodeRuntime
{
Id = stationNodeId,
SystemId = station.SystemId,
Kind = "station",
Position = station.Position,
BubbleId = stationBubbleId,
ParentNodeId = placement.AnchorNode.Id,
OccupyingStructureId = station.Id,
});
localBubbles.Add(new LocalBubbleRuntime
{
Id = stationBubbleId,
NodeId = stationNodeId,
SystemId = station.SystemId,
Radius = MathF.Max(160f, definition.Radius + 60f),
});
localBubbles[^1].OccupantStationIds.Add(station.Id);
placement.AnchorNode.OccupyingStructureId = station.Id;
foreach (var moduleId in definition.Modules)
{
@@ -152,8 +216,13 @@ public sealed class ScenarioLoader
foreach (var station in stations)
{
InitializeStationPopulation(station);
station.Inventory["fuel"] = 240f;
station.Inventory["refined-metals"] = 120f;
if (station.Population > 0f)
{
station.Inventory["water"] = MathF.Max(80f, station.Population * 1.5f);
}
}
var refinery = stations.FirstOrDefault((station) =>
@@ -187,8 +256,9 @@ public sealed class ScenarioLoader
FactionId = formation.FactionId ?? DefaultFactionId,
Position = position,
TargetPosition = position,
SpatialState = CreateInitialShipSpatialState(formation.SystemId, position, spatialNodes),
DefaultBehavior = CreateBehavior(definition, formation.SystemId, scenario, patrolRoutes, refinery),
ControllerTask = new ControllerTaskRuntime { Kind = "idle", Threshold = balance.ArrivalThreshold },
ControllerTask = new ControllerTaskRuntime { Kind = "idle", Threshold = balance.ArrivalThreshold, Status = "pending" },
Health = definition.MaxHealth,
});
@@ -209,6 +279,11 @@ public sealed class ScenarioLoader
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, spatialNodes, now);
var (constructionSites, marketOrders) = CreateConstructionSites(stations, claims, spatialNodes, moduleRecipeDefinitions);
return new SimulationWorld
{
@@ -217,12 +292,20 @@ public sealed class ScenarioLoader
Balance = balance,
Systems = systemRuntimes,
Nodes = nodes,
SpatialNodes = spatialNodes,
LocalBubbles = localBubbles,
Stations = stations,
Ships = shipsRuntime,
Factions = factions,
Commanders = commanders,
Claims = claims,
ConstructionSites = constructionSites,
MarketOrders = marketOrders,
Policies = policies,
ShipDefinitions = shipDefinitions,
ItemDefinitions = itemDefinitions,
ModuleRecipes = moduleRecipeDefinitions,
Recipes = recipeDefinitions,
GeneratedAtUtc = DateTimeOffset.UtcNow,
};
}
@@ -810,6 +893,442 @@ public sealed class ScenarioLoader
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
{
var nodes = new List<NodeRuntime>();
var bubbles = new List<LocalBubbleRuntime>();
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, NodeRuntime>>();
var starNode = AddSpatialNode(
nodes,
bubbles,
id: $"node-{system.Definition.Id}-star",
systemId: system.Definition.Id,
kind: "star",
position: Vector3.Zero,
radius: MathF.Max(system.Definition.GravityWellRadius + StarBubbleRadiusPadding, 180f));
for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
{
var planet = system.Definition.Planets[planetIndex];
var planetNodeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}";
var planetPosition = ComputePlanetPosition(planet);
var planetNode = AddSpatialNode(
nodes,
bubbles,
id: planetNodeId,
systemId: system.Definition.Id,
kind: "planet",
position: planetPosition,
radius: MathF.Max(planet.Size + PlanetBubbleRadiusPadding, 120f),
parentNodeId: starNode.Id);
var lagrangeNodes = new Dictionary<string, NodeRuntime>(StringComparer.Ordinal);
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet.OrbitRadius, planetIndex))
{
var lagrangeNode = AddSpatialNode(
nodes,
bubbles,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}",
systemId: system.Definition.Id,
kind: "lagrange-point",
position: point.Position,
radius: LagrangeBubbleRadius,
parentNodeId: planetNode.Id,
orbitReferenceId: point.Designation);
lagrangeNodes[point.Designation] = lagrangeNode;
}
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
if (planet.MoonCount <= 0)
{
continue;
}
var moonOrbitRadius = MathF.Max(planet.Size + 48f, 42f);
for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1)
{
var moonPosition = ComputeMoonPosition(planetPosition, moonOrbitRadius, moonIndex, planetIndex);
AddSpatialNode(
nodes,
bubbles,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
systemId: system.Definition.Id,
kind: "moon",
position: moonPosition,
radius: MoonBubbleRadiusPadding + 24f,
parentNodeId: planetNode.Id);
moonOrbitRadius += 30f;
}
}
return new SystemSpatialGraph(system.Definition.Id, nodes, bubbles, lagrangeNodesByPlanetIndex);
}
private static NodeRuntime AddSpatialNode(
ICollection<NodeRuntime> nodes,
ICollection<LocalBubbleRuntime> bubbles,
string id,
string systemId,
string kind,
Vector3 position,
float radius,
string? parentNodeId = null,
string? orbitReferenceId = null)
{
var bubbleId = $"bubble-{id}";
var node = new NodeRuntime
{
Id = id,
SystemId = systemId,
Kind = kind,
Position = position,
BubbleId = bubbleId,
ParentNodeId = parentNodeId,
OrbitReferenceId = orbitReferenceId,
};
nodes.Add(node);
bubbles.Add(new LocalBubbleRuntime
{
Id = bubbleId,
NodeId = id,
SystemId = systemId,
Radius = radius,
});
return node;
}
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(Vector3 planetPosition, float orbitRadius, int planetIndex)
{
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
var tangential = new Vector3(-radial.Z, 0f, radial.X);
var offset = MathF.Max(orbitRadius * 0.18f, 72f + (planetIndex * 6f));
var triangularAngle = MathF.PI / 3f;
yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadius));
yield return new LagrangePointPlacement(
"L4",
Add(Scale(radial, orbitRadius * MathF.Cos(triangularAngle)), Scale(tangential, orbitRadius * MathF.Sin(triangularAngle))));
yield return new LagrangePointPlacement(
"L5",
Add(Scale(radial, orbitRadius * MathF.Cos(triangularAngle)), Scale(tangential, -orbitRadius * MathF.Sin(triangularAngle))));
}
private static StationPlacement ResolveStationPlacement(
InitialStationDefinition plan,
SystemRuntime system,
SystemSpatialGraph graph,
IReadOnlyCollection<NodeRuntime> existingNodes)
{
if (plan.PlanetIndex is int planetIndex &&
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
{
var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
if (lagrangeNodes.TryGetValue(designation, out var lagrangeNode))
{
return new StationPlacement(lagrangeNode, lagrangeNode.Position);
}
}
if (plan.Position is { Length: 3 })
{
var targetPosition = NormalizeScenarioPoint(system, plan.Position);
var preferredNode = existingNodes
.Where((node) => node.SystemId == system.Definition.Id && node.Kind == "lagrange-point")
.OrderBy((node) => node.Position.DistanceTo(targetPosition))
.FirstOrDefault()
?? existingNodes
.Where((node) => node.SystemId == system.Definition.Id)
.OrderBy((node) => node.Position.DistanceTo(targetPosition))
.First();
return new StationPlacement(preferredNode, preferredNode.Position);
}
var fallbackNode = graph.Nodes
.FirstOrDefault((node) => node.Kind == "lagrange-point" && string.IsNullOrEmpty(node.OccupyingStructureId))
?? graph.Nodes.First((node) => node.Kind == "planet");
return new StationPlacement(fallbackNode, fallbackNode.Position);
}
private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch
{
< 0 => "L4",
> 0 => "L5",
_ => "L1",
};
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
{
var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
var x = MathF.Cos(angle) * planet.OrbitRadius;
var z = MathF.Sin(angle) * planet.OrbitRadius;
return new Vector3(x, 0f, z);
}
private static Vector3 ComputeMoonPosition(Vector3 planetPosition, float orbitRadius, int moonIndex, int planetIndex)
{
var angle = ((MathF.PI * 2f) / MathF.Max(1, moonIndex + 3)) * (moonIndex + 1) + (planetIndex * 0.37f);
return Add(planetPosition, new Vector3(MathF.Cos(angle) * orbitRadius, 0f, MathF.Sin(angle) * orbitRadius));
}
private static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<NodeRuntime> nodes)
{
var nearestNode = nodes
.Where((node) => node.SystemId == systemId)
.OrderBy((node) => node.Position.DistanceTo(position))
.FirstOrDefault();
return new ShipSpatialStateRuntime
{
CurrentSystemId = systemId,
SpaceLayer = SpaceLayerKinds.LocalSpace,
CurrentNodeId = nearestNode?.Id,
CurrentBubbleId = nearestNode?.BubbleId,
LocalPosition = position,
SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight,
};
}
private static List<ClaimRuntime> CreateClaims(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<NodeRuntime> nodes,
DateTimeOffset nowUtc)
{
var claims = new List<ClaimRuntime>(stations.Count);
foreach (var station in stations)
{
if (station.AnchorNodeId is null)
{
continue;
}
var anchorNode = nodes.FirstOrDefault((node) => node.Id == station.AnchorNodeId);
if (anchorNode is null)
{
continue;
}
claims.Add(new ClaimRuntime
{
Id = $"claim-{station.Id}",
FactionId = station.FactionId,
SystemId = station.SystemId,
NodeId = anchorNode.Id,
BubbleId = anchorNode.BubbleId,
PlacedAtUtc = nowUtc,
ActivatesAtUtc = nowUtc.AddSeconds(8),
State = ClaimStateKinds.Activating,
Health = 100f,
});
}
return claims;
}
private static (List<ConstructionSiteRuntime> ConstructionSites, List<MarketOrderRuntime> MarketOrders) CreateConstructionSites(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<ClaimRuntime> claims,
IReadOnlyCollection<NodeRuntime> nodes,
IReadOnlyDictionary<string, ModuleRecipeDefinition> moduleRecipes)
{
var sites = new List<ConstructionSiteRuntime>();
var orders = new List<MarketOrderRuntime>();
foreach (var station in stations)
{
var moduleId = GetNextConstructionSiteModule(station, moduleRecipes);
if (moduleId is null || station.AnchorNodeId is null)
{
continue;
}
var anchorNode = nodes.FirstOrDefault((node) => node.Id == station.AnchorNodeId);
var claim = claims.FirstOrDefault((candidate) => candidate.Id == $"claim-{station.Id}");
if (anchorNode is null || claim is null || !moduleRecipes.TryGetValue(moduleId, out var recipe))
{
continue;
}
var site = new ConstructionSiteRuntime
{
Id = $"site-{station.Id}",
FactionId = station.FactionId,
SystemId = station.SystemId,
NodeId = anchorNode.Id,
BubbleId = anchorNode.BubbleId,
TargetKind = "station-module",
TargetDefinitionId = station.Definition.Id,
BlueprintId = moduleId,
ClaimId = claim.Id,
StationId = station.Id,
State = claim.State == ClaimStateKinds.Active ? ConstructionSiteStateKinds.Active : ConstructionSiteStateKinds.Planned,
};
foreach (var input in recipe.Inputs)
{
site.RequiredItems[input.ItemId] = input.Amount;
site.DeliveredItems[input.ItemId] = 0f;
var orderId = $"market-order-{station.Id}-{moduleId}-{input.ItemId}";
site.MarketOrderIds.Add(orderId);
station.MarketOrderIds.Add(orderId);
orders.Add(new MarketOrderRuntime
{
Id = orderId,
FactionId = station.FactionId,
StationId = station.Id,
ConstructionSiteId = site.Id,
Kind = MarketOrderKinds.Buy,
ItemId = input.ItemId,
Amount = input.Amount,
RemainingAmount = input.Amount,
Valuation = 1f,
State = MarketOrderStateKinds.Open,
});
}
sites.Add(site);
}
return (sites, orders);
}
private static string? GetNextConstructionSiteModule(
StationRuntime station,
IReadOnlyDictionary<string, ModuleRecipeDefinition> moduleRecipes)
{
foreach (var moduleId in new[] { "gas-tank", "fuel-processor", "refinery-stack", "dock-bay-small" })
{
if (!station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)
&& moduleRecipes.ContainsKey(moduleId))
{
return moduleId;
}
}
return null;
}
private static void InitializeStationPopulation(StationRuntime station)
{
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
station.PopulationCapacity = 40f + (habitatModules * 220f);
station.WorkforceRequired = MathF.Max(12f, station.InstalledModules.Count * 14f);
station.Population = habitatModules > 0
? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f)
: MathF.Min(28f, station.PopulationCapacity);
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
}
private static List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
{
var policies = new List<PolicySetRuntime>(factions.Count);
foreach (var faction in factions)
{
var policyId = $"policy-{faction.Id}";
faction.DefaultPolicySetId = policyId;
policies.Add(new PolicySetRuntime
{
Id = policyId,
OwnerKind = "faction",
OwnerId = faction.Id,
});
}
return policies;
}
private static List<CommanderRuntime> CreateCommanders(
IReadOnlyCollection<FactionRuntime> factions,
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<ShipRuntime> ships)
{
var commanders = new List<CommanderRuntime>();
var factionCommanders = new Dictionary<string, CommanderRuntime>(StringComparer.Ordinal);
var factionsById = factions.ToDictionary((faction) => faction.Id, StringComparer.Ordinal);
foreach (var faction in factions)
{
var commander = new CommanderRuntime
{
Id = $"commander-faction-{faction.Id}",
Kind = CommanderKind.Faction,
FactionId = faction.Id,
ControlledEntityId = faction.Id,
PolicySetId = faction.DefaultPolicySetId,
Doctrine = "strategic-default",
};
commanders.Add(commander);
factionCommanders[faction.Id] = commander;
faction.CommanderIds.Add(commander.Id);
}
foreach (var station in stations)
{
if (!factionCommanders.TryGetValue(station.FactionId, out var parentCommander))
{
continue;
}
var commander = new CommanderRuntime
{
Id = $"commander-station-{station.Id}",
Kind = CommanderKind.Station,
FactionId = station.FactionId,
ParentCommanderId = parentCommander.Id,
ControlledEntityId = station.Id,
PolicySetId = parentCommander.PolicySetId,
Doctrine = "station-default",
};
station.CommanderId = commander.Id;
station.PolicySetId = parentCommander.PolicySetId;
parentCommander.SubordinateCommanderIds.Add(commander.Id);
factionsById[station.FactionId].CommanderIds.Add(commander.Id);
commanders.Add(commander);
}
foreach (var ship in ships)
{
if (!factionCommanders.TryGetValue(ship.FactionId, out var parentCommander))
{
continue;
}
var commander = new CommanderRuntime
{
Id = $"commander-ship-{ship.Id}",
Kind = CommanderKind.Ship,
FactionId = ship.FactionId,
ParentCommanderId = parentCommander.Id,
ControlledEntityId = ship.Id,
PolicySetId = parentCommander.PolicySetId,
Doctrine = "ship-default",
ActiveBehavior = CopyBehavior(ship.DefaultBehavior),
ActiveTask = CopyTask(ship.ControllerTask, null),
};
if (ship.Order is not null)
{
commander.ActiveOrder = CopyOrder(ship.Order);
}
ship.CommanderId = commander.Id;
ship.PolicySetId = parentCommander.PolicySetId;
parentCommander.SubordinateCommanderIds.Add(commander.Id);
factionsById[ship.FactionId].CommanderIds.Add(commander.Id);
commanders.Add(commander);
}
return commanders;
}
private static string ToFactionLabel(string factionId)
{
return string.Join(" ",
@@ -880,26 +1399,6 @@ public sealed class ScenarioLoader
};
}
private static Vector3 ResolveStationPosition(SystemRuntime system, InitialStationDefinition plan, BalanceDefinition balance)
{
if (plan.Position is { Length: 3 })
{
return NormalizeScenarioPoint(system, plan.Position);
}
if (plan.PlanetIndex is int planetIndex && planetIndex >= 0 && planetIndex < system.Definition.Planets.Count)
{
var planet = system.Definition.Planets[planetIndex];
var side = plan.LagrangeSide ?? 1;
return new Vector3(
planet.OrbitRadius + (side * 72f),
balance.YPlane,
(planetIndex + 1) * 42f * side);
}
return new Vector3(180f, balance.YPlane, 0f);
}
private static Vector3 ToVector(float[] values) => new(values[0], values[1], values[2]);
private static Vector3 NormalizeScenarioPoint(SystemRuntime system, float[] values)
@@ -925,4 +1424,73 @@ public sealed class ScenarioLoader
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
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 CommanderBehaviorRuntime CopyBehavior(DefaultBehaviorRuntime behavior) => new()
{
Kind = behavior.Kind,
AreaSystemId = behavior.AreaSystemId,
ModuleId = behavior.ModuleId,
NodeId = behavior.NodeId,
Phase = behavior.Phase,
PatrolIndex = behavior.PatrolIndex,
StationId = behavior.StationId,
};
private static CommanderOrderRuntime CopyOrder(ShipOrderRuntime order) => new()
{
Kind = order.Kind,
Status = order.Status,
DestinationSystemId = order.DestinationSystemId,
DestinationPosition = order.DestinationPosition,
};
private static CommanderTaskRuntime CopyTask(ControllerTaskRuntime task, string? targetNodeId) => new()
{
Kind = task.Kind,
Status = task.Status,
TargetEntityId = task.TargetEntityId,
TargetNodeId = targetNodeId ?? task.TargetNodeId,
TargetPosition = task.TargetPosition,
TargetSystemId = task.TargetSystemId,
Threshold = task.Threshold,
};
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);
}
private sealed record SystemSpatialGraph(
string SystemId,
List<NodeRuntime> Nodes,
List<LocalBubbleRuntime> Bubbles,
Dictionary<int, Dictionary<string, NodeRuntime>> LagrangeNodesByPlanetIndex);
private sealed record LagrangePointPlacement(string Designation, Vector3 Position);
private sealed record StationPlacement(NodeRuntime AnchorNode, Vector3 Position);
}