Complete universe model migration

This commit is contained in:
2026-04-07 14:16:59 -04:00
parent d0c6e30304
commit 6c92ab50c8
76 changed files with 2061 additions and 1072 deletions

View File

@@ -2,8 +2,15 @@ using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
namespace SpaceGame.Api.Universe.Scenario;
public sealed class SpatialBuilder(IBalanceService balance)
public sealed class SpatialBuilder
{
internal static bool IsConstructibleAnchorKind(SpatialNodeKind kind) => kind is SpatialNodeKind.Planet or SpatialNodeKind.Moon or SpatialNodeKind.LagrangePoint;
internal static string? ResolveCompatibleCelestialId(AnchorRuntime? anchor) =>
anchor is not null && string.Equals(anchor.SourceEntityKind, "celestial", StringComparison.Ordinal)
? anchor.SourceEntityId
: null;
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems)
{
var systemGraphs = systems.ToDictionary(
@@ -11,6 +18,19 @@ public sealed class SpatialBuilder(IBalanceService balance)
BuildSystemSpatialGraph,
StringComparer.Ordinal);
var celestials = systemGraphs.Values.SelectMany(graph => graph.Celestials).ToList();
var anchors = celestials.Select(celestial => new AnchorRuntime
{
Id = celestial.Id,
SystemId = celestial.SystemId,
Kind = celestial.Kind,
Position = celestial.Position,
LocalSpaceRadius = celestial.LocalSpaceRadius,
ParentAnchorId = celestial.ParentAnchorId,
OrbitReferenceId = celestial.OrbitReferenceId,
OccupyingStructureId = celestial.OccupyingStructureId,
SourceEntityKind = "celestial",
SourceEntityId = celestial.Id,
}).ToList();
var nodes = new List<ResourceNodeRuntime>();
var nodeIdCounter = 0;
@@ -20,24 +40,43 @@ public sealed class SpatialBuilder(IBalanceService balance)
foreach (var node in system.Definition.ResourceNodes)
{
var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node);
var nodeId = $"node-{++nodeIdCounter}";
var localPosition = ComputeResourceNodeLocalPosition(node);
var anchorPosition = anchorCelestial is null
? localPosition
: Add(anchorCelestial.Position, localPosition);
nodes.Add(new ResourceNodeRuntime
{
Id = $"node-{++nodeIdCounter}",
Id = nodeId,
AnchorId = nodeId,
SystemId = system.Definition.Id,
Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane),
Position = localPosition,
SourceKind = node.SourceKind,
ItemId = node.ItemId,
CelestialId = anchorCelestial?.Id,
LocalSpaceRadius = LocalSpaceRadius,
OrbitRadius = node.RadiusOffset,
OrbitPhase = node.Angle,
OrbitInclination = DegreesToRadians(node.InclinationDegrees),
OreRemaining = node.OreAmount,
MaxOre = node.OreAmount,
});
nodes[^1].Deposits.AddRange(BuildResourceDeposits(system.Definition.Id, nodeId, node, node.OreAmount));
anchors.Add(new AnchorRuntime
{
Id = nodeId,
SystemId = system.Definition.Id,
Kind = SpatialNodeKind.ResourceNode,
Position = anchorPosition,
LocalSpaceRadius = LocalSpaceRadius,
ParentAnchorId = anchorCelestial?.Id,
SourceEntityKind = "resource-node",
SourceEntityId = nodeId,
});
}
}
return new ScenarioSpatialLayout(systemGraphs, celestials, nodes);
return new ScenarioSpatialLayout(systemGraphs, anchors, celestials, nodes);
}
private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
@@ -70,7 +109,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
kind: SpatialNodeKind.Planet,
position: planetPosition,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: primaryStarNodeId);
parentAnchorId: primaryStarNodeId);
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
@@ -82,7 +121,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
kind: SpatialNodeKind.LagrangePoint,
position: point.Position,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id,
parentAnchorId: planetCelestial.Id,
orbitReferenceId: point.Designation);
lagrangeNodes[point.Designation] = lagrangeCelestial;
}
@@ -100,7 +139,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
kind: SpatialNodeKind.Moon,
position: moonPosition,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id);
parentAnchorId: planetCelestial.Id);
}
}
@@ -114,7 +153,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
SpatialNodeKind kind,
Vector3 position,
float localSpaceRadius,
string? parentNodeId = null,
string? parentAnchorId = null,
string? orbitReferenceId = null)
{
var celestial = new CelestialRuntime
@@ -124,7 +163,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
Kind = kind,
Position = position,
LocalSpaceRadius = localSpaceRadius,
ParentNodeId = parentNodeId,
ParentAnchorId = parentAnchorId,
OrbitReferenceId = orbitReferenceId,
};
@@ -183,7 +222,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
InitialStationDefinition plan,
SystemRuntime system,
SystemSpatialGraph graph,
IReadOnlyCollection<CelestialRuntime> existingCelestials)
IReadOnlyCollection<AnchorRuntime> existingAnchors)
{
if (plan.PlanetIndex is int planetIndex &&
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
@@ -191,28 +230,32 @@ public sealed class SpatialBuilder(IBalanceService balance)
var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
if (lagrangeNodes.TryGetValue(designation, out var lagrangeCelestial))
{
return new StationPlacement(lagrangeCelestial, lagrangeCelestial.Position);
var lagrangeAnchor = existingAnchors.First(anchor => string.Equals(anchor.Id, lagrangeCelestial.Id, StringComparison.Ordinal));
return new StationPlacement(lagrangeAnchor, lagrangeCelestial, lagrangeAnchor.Position);
}
}
if (plan.Position is { Length: 3 })
{
var targetPosition = NormalizeScenarioPoint(system, plan.Position);
var preferredCelestial = existingCelestials
.Where(c => c.SystemId == system.Definition.Id && c.Kind == SpatialNodeKind.LagrangePoint)
.OrderBy(c => c.Position.DistanceTo(targetPosition))
var preferredAnchor = existingAnchors
.Where(anchor => anchor.SystemId == system.Definition.Id && anchor.Kind == SpatialNodeKind.LagrangePoint)
.OrderBy(anchor => anchor.Position.DistanceTo(targetPosition))
.FirstOrDefault()
?? existingCelestials
.Where(c => c.SystemId == system.Definition.Id)
.OrderBy(c => c.Position.DistanceTo(targetPosition))
?? existingAnchors
.Where(anchor => anchor.SystemId == system.Definition.Id && IsConstructibleAnchorKind(anchor.Kind))
.OrderBy(anchor => anchor.Position.DistanceTo(targetPosition))
.First();
return new StationPlacement(preferredCelestial, preferredCelestial.Position);
var preferredCelestial = graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, ResolveCompatibleCelestialId(preferredAnchor), StringComparison.Ordinal));
return new StationPlacement(preferredAnchor, preferredCelestial, preferredAnchor.Position);
}
var fallbackCelestial = graph.Celestials
.FirstOrDefault(c => c.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(c.OccupyingStructureId))
?? graph.Celestials.First(c => c.Kind == SpatialNodeKind.Planet);
return new StationPlacement(fallbackCelestial, fallbackCelestial.Position);
var fallbackAnchor = existingAnchors
.Where(anchor => anchor.SystemId == system.Definition.Id)
.FirstOrDefault(anchor => anchor.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(anchor.OccupyingStructureId))
?? existingAnchors.First(anchor => anchor.SystemId == system.Definition.Id && anchor.Kind == SpatialNodeKind.Planet);
var fallbackCelestial = graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, ResolveCompatibleCelestialId(fallbackAnchor), StringComparison.Ordinal));
return new StationPlacement(fallbackAnchor, fallbackCelestial, fallbackAnchor.Position);
}
private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch
@@ -256,20 +299,80 @@ public sealed class SpatialBuilder(IBalanceService balance)
return graph.Celestials.FirstOrDefault(c => c.Id == planetNodeId);
}
private static Vector3 ComputeResourceNodePosition(CelestialRuntime? anchorCelestial, ResourceNodeDefinition definition, float yPlane)
private static Vector3 ComputeResourceNodeLocalPosition(ResourceNodeDefinition definition)
{
var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f);
var offset = new Vector3(
return new Vector3(
MathF.Cos(definition.Angle) * definition.RadiusOffset,
verticalOffset,
MathF.Sin(definition.Angle) * definition.RadiusOffset);
}
if (anchorCelestial is null)
private static IReadOnlyList<ResourceDepositRuntime> BuildResourceDeposits(
string systemId,
string nodeId,
ResourceNodeDefinition definition,
float oreAmount)
{
var depositCount = Math.Clamp((int)MathF.Round(MathF.Sqrt(MathF.Max(oreAmount, 1f)) / 18f), 4, 12);
var deposits = new List<ResourceDepositRuntime>(depositCount);
var weightTotal = 0f;
var weights = new float[depositCount];
for (var index = 0; index < depositCount; index += 1)
{
return new Vector3(offset.X, yPlane + offset.Y, offset.Z);
var weight = 0.8f + (Hash01(systemId, nodeId, $"weight-{index}") * 1.6f);
weights[index] = weight;
weightTotal += weight;
}
return Add(anchorCelestial.Position, offset);
var scatterRadius = MathF.Max(140f, LocalSpaceRadius * 0.58f);
for (var index = 0; index < depositCount; index += 1)
{
var angle = Hash01(systemId, nodeId, $"angle-{index}") * MathF.PI * 2f;
var radiusFactor = 0.22f + (Hash01(systemId, nodeId, $"radius-{index}") * 0.74f);
var radius = scatterRadius * MathF.Sqrt(radiusFactor);
var vertical = (Hash01(systemId, nodeId, $"vertical-{index}") - 0.5f) * MathF.Max(60f, scatterRadius * 0.14f);
var localPosition = new Vector3(
MathF.Cos(angle) * radius,
vertical,
MathF.Sin(angle) * radius);
var maxOre = oreAmount * (weights[index] / MathF.Max(weightTotal, 0.001f));
deposits.Add(new ResourceDepositRuntime
{
Id = $"{nodeId}-deposit-{index + 1}",
NodeId = nodeId,
AnchorId = nodeId,
Position = localPosition,
OreRemaining = maxOre,
MaxOre = maxOre,
});
}
return deposits;
}
private static float Hash01(string systemId, string nodeId, string salt)
{
unchecked
{
var hash = 17;
foreach (var character in systemId)
{
hash = (hash * 31) + character;
}
foreach (var character in nodeId)
{
hash = (hash * 31) + character;
}
foreach (var character in salt)
{
hash = (hash * 31) + character;
}
return (hash & 0x7fffffff) / (float)int.MaxValue;
}
}
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
@@ -286,19 +389,22 @@ public sealed class SpatialBuilder(IBalanceService balance)
return Add(planetPosition, local);
}
internal static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<CelestialRuntime> celestials)
internal static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<AnchorRuntime> anchors)
{
var nearestCelestial = celestials
.Where(c => c.SystemId == systemId)
.OrderBy(c => c.Position.DistanceTo(position))
var nearestAnchor = anchors
.Where(anchor => anchor.SystemId == systemId)
.OrderBy(anchor => anchor.Position.DistanceTo(position))
.FirstOrDefault();
var localPosition = nearestAnchor is null
? position
: position.Subtract(nearestAnchor.Position);
return new ShipSpatialStateRuntime
{
CurrentSystemId = systemId,
SpaceLayer = SpaceLayerKind.LocalSpace,
CurrentCelestialId = nearestCelestial?.Id,
LocalPosition = position,
CurrentAnchorId = nearestAnchor?.Id,
LocalPosition = localPosition,
SystemPosition = position,
MovementRegime = MovementRegimeKind.LocalFlight,
};
@@ -307,6 +413,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
public sealed record ScenarioSpatialLayout(
IReadOnlyDictionary<string, SystemSpatialGraph> SystemGraphs,
List<AnchorRuntime> Anchors,
List<CelestialRuntime> Celestials,
List<ResourceNodeRuntime> Nodes);
@@ -317,4 +424,4 @@ public sealed record SystemSpatialGraph(
internal sealed record LagrangePointPlacement(string Designation, Vector3 Position);
internal sealed record StationPlacement(CelestialRuntime AnchorCelestial, Vector3 Position);
internal sealed record StationPlacement(AnchorRuntime Anchor, CelestialRuntime? Celestial, Vector3 Position);