428 lines
18 KiB
C#
428 lines
18 KiB
C#
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
|
|
|
namespace SpaceGame.Api.Universe.Scenario;
|
|
|
|
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(
|
|
system => system.Definition.Id,
|
|
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;
|
|
|
|
foreach (var system in systems)
|
|
{
|
|
var systemGraph = systemGraphs[system.Definition.Id];
|
|
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 = nodeId,
|
|
AnchorId = nodeId,
|
|
SystemId = system.Definition.Id,
|
|
Position = localPosition,
|
|
SourceKind = node.SourceKind,
|
|
ItemId = node.ItemId,
|
|
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, anchors, celestials, nodes);
|
|
}
|
|
|
|
private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
|
|
{
|
|
var celestials = new List<CelestialRuntime>();
|
|
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();
|
|
|
|
for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
|
|
{
|
|
AddCelestial(
|
|
celestials,
|
|
id: $"node-{system.Definition.Id}-star-{starIndex + 1}",
|
|
systemId: system.Definition.Id,
|
|
kind: SpatialNodeKind.Star,
|
|
position: Vector3.Zero,
|
|
localSpaceRadius: LocalSpaceRadius);
|
|
}
|
|
|
|
var primaryStarNodeId = $"node-{system.Definition.Id}-star-1";
|
|
|
|
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 planetCelestial = AddCelestial(
|
|
celestials,
|
|
id: planetNodeId,
|
|
systemId: system.Definition.Id,
|
|
kind: SpatialNodeKind.Planet,
|
|
position: planetPosition,
|
|
localSpaceRadius: LocalSpaceRadius,
|
|
parentAnchorId: primaryStarNodeId);
|
|
|
|
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
|
|
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
|
|
{
|
|
var lagrangeCelestial = AddCelestial(
|
|
celestials,
|
|
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}",
|
|
systemId: system.Definition.Id,
|
|
kind: SpatialNodeKind.LagrangePoint,
|
|
position: point.Position,
|
|
localSpaceRadius: LocalSpaceRadius,
|
|
parentAnchorId: planetCelestial.Id,
|
|
orbitReferenceId: point.Designation);
|
|
lagrangeNodes[point.Designation] = lagrangeCelestial;
|
|
}
|
|
|
|
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
|
|
|
|
for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
|
|
{
|
|
var moon = planet.Moons[moonIndex];
|
|
var moonPosition = ComputeMoonPosition(planetPosition, moon);
|
|
AddCelestial(
|
|
celestials,
|
|
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
|
|
systemId: system.Definition.Id,
|
|
kind: SpatialNodeKind.Moon,
|
|
position: moonPosition,
|
|
localSpaceRadius: LocalSpaceRadius,
|
|
parentAnchorId: planetCelestial.Id);
|
|
}
|
|
}
|
|
|
|
return new SystemSpatialGraph(system.Definition.Id, celestials, lagrangeNodesByPlanetIndex);
|
|
}
|
|
|
|
private static CelestialRuntime AddCelestial(
|
|
ICollection<CelestialRuntime> celestials,
|
|
string id,
|
|
string systemId,
|
|
SpatialNodeKind kind,
|
|
Vector3 position,
|
|
float localSpaceRadius,
|
|
string? parentAnchorId = null,
|
|
string? orbitReferenceId = null)
|
|
{
|
|
var celestial = new CelestialRuntime
|
|
{
|
|
Id = id,
|
|
SystemId = systemId,
|
|
Kind = kind,
|
|
Position = position,
|
|
LocalSpaceRadius = localSpaceRadius,
|
|
ParentAnchorId = parentAnchorId,
|
|
OrbitReferenceId = orbitReferenceId,
|
|
};
|
|
|
|
celestials.Add(celestial);
|
|
return celestial;
|
|
}
|
|
|
|
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(Vector3 planetPosition, PlanetDefinition planet)
|
|
{
|
|
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
|
|
var tangential = new Vector3(-radial.Z, 0f, radial.X);
|
|
var orbitRadiusKm = MathF.Sqrt(planetPosition.X * planetPosition.X + planetPosition.Z * planetPosition.Z);
|
|
var offset = ComputePlanetLocalLagrangeOffset(orbitRadiusKm, planet);
|
|
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, -orbitRadiusKm));
|
|
yield return new LagrangePointPlacement(
|
|
"L4",
|
|
Add(
|
|
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
|
|
Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle))));
|
|
yield return new LagrangePointPlacement(
|
|
"L5",
|
|
Add(
|
|
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
|
|
Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle))));
|
|
}
|
|
|
|
private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet)
|
|
{
|
|
var planetMassProxy = EstimatePlanetMassRatio(planet);
|
|
var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f));
|
|
var minimumOffset = MathF.Max(planet.Size * 4f, 25000f);
|
|
return MathF.Max(minimumOffset, hillLikeOffset);
|
|
}
|
|
|
|
private static float EstimatePlanetMassRatio(PlanetDefinition planet)
|
|
{
|
|
var earthRadiusRatio = MathF.Max(planet.Size / 6371f, 0.05f);
|
|
var densityFactor = planet.PlanetType switch
|
|
{
|
|
"gas-giant" => 0.24f,
|
|
"ice-giant" => 0.18f,
|
|
"oceanic" => 0.95f,
|
|
"ice" => 0.7f,
|
|
_ => 1f,
|
|
};
|
|
|
|
var earthMasses = MathF.Pow(earthRadiusRatio, 3f) * densityFactor;
|
|
return earthMasses / 332_946f;
|
|
}
|
|
|
|
internal static StationPlacement ResolveStationPlacement(
|
|
InitialStationDefinition plan,
|
|
SystemRuntime system,
|
|
SystemSpatialGraph graph,
|
|
IReadOnlyCollection<AnchorRuntime> existingAnchors)
|
|
{
|
|
if (plan.PlanetIndex is int planetIndex &&
|
|
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
|
|
{
|
|
var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
|
|
if (lagrangeNodes.TryGetValue(designation, out var lagrangeCelestial))
|
|
{
|
|
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 preferredAnchor = existingAnchors
|
|
.Where(anchor => anchor.SystemId == system.Definition.Id && anchor.Kind == SpatialNodeKind.LagrangePoint)
|
|
.OrderBy(anchor => anchor.Position.DistanceTo(targetPosition))
|
|
.FirstOrDefault()
|
|
?? existingAnchors
|
|
.Where(anchor => anchor.SystemId == system.Definition.Id && IsConstructibleAnchorKind(anchor.Kind))
|
|
.OrderBy(anchor => anchor.Position.DistanceTo(targetPosition))
|
|
.First();
|
|
var preferredCelestial = graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, ResolveCompatibleCelestialId(preferredAnchor), StringComparison.Ordinal));
|
|
return new StationPlacement(preferredAnchor, preferredCelestial, preferredAnchor.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
|
|
{
|
|
< 0 => "L4",
|
|
> 0 => "L5",
|
|
_ => "L1",
|
|
};
|
|
|
|
private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(definition.AnchorReference))
|
|
{
|
|
var anchorId = definition.AnchorReference.ToLowerInvariant() switch
|
|
{
|
|
var reference when reference.StartsWith("star-", StringComparison.Ordinal)
|
|
=> $"node-{graph.SystemId}-{reference}",
|
|
var reference when reference.StartsWith("planet-", StringComparison.Ordinal)
|
|
=> $"node-{graph.SystemId}-{reference}",
|
|
_ => null,
|
|
};
|
|
|
|
if (anchorId is not null)
|
|
{
|
|
return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal));
|
|
}
|
|
}
|
|
|
|
if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (definition.AnchorMoonIndex is int moonIndex && moonIndex >= 0)
|
|
{
|
|
var moonNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
|
|
return graph.Celestials.FirstOrDefault(c => c.Id == moonNodeId);
|
|
}
|
|
|
|
var planetNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}";
|
|
return graph.Celestials.FirstOrDefault(c => c.Id == planetNodeId);
|
|
}
|
|
|
|
private static Vector3 ComputeResourceNodeLocalPosition(ResourceNodeDefinition definition)
|
|
{
|
|
var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f);
|
|
return new Vector3(
|
|
MathF.Cos(definition.Angle) * definition.RadiusOffset,
|
|
verticalOffset,
|
|
MathF.Sin(definition.Angle) * definition.RadiusOffset);
|
|
}
|
|
|
|
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)
|
|
{
|
|
var weight = 0.8f + (Hash01(systemId, nodeId, $"weight-{index}") * 1.6f);
|
|
weights[index] = weight;
|
|
weightTotal += weight;
|
|
}
|
|
|
|
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)
|
|
{
|
|
var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
|
|
var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius);
|
|
return new Vector3(MathF.Cos(angle) * orbitRadiusKm, 0f, MathF.Sin(angle) * orbitRadiusKm);
|
|
}
|
|
|
|
private static Vector3 ComputeMoonPosition(Vector3 planetPosition, MoonDefinition moon)
|
|
{
|
|
var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch);
|
|
var local = new Vector3(MathF.Cos(angle) * moon.OrbitRadius, 0f, MathF.Sin(angle) * moon.OrbitRadius);
|
|
return Add(planetPosition, local);
|
|
}
|
|
|
|
internal static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<AnchorRuntime> anchors)
|
|
{
|
|
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,
|
|
CurrentAnchorId = nearestAnchor?.Id,
|
|
LocalPosition = localPosition,
|
|
SystemPosition = position,
|
|
MovementRegime = MovementRegimeKind.LocalFlight,
|
|
};
|
|
}
|
|
}
|
|
|
|
public sealed record ScenarioSpatialLayout(
|
|
IReadOnlyDictionary<string, SystemSpatialGraph> SystemGraphs,
|
|
List<AnchorRuntime> Anchors,
|
|
List<CelestialRuntime> Celestials,
|
|
List<ResourceNodeRuntime> Nodes);
|
|
|
|
public sealed record SystemSpatialGraph(
|
|
string SystemId,
|
|
List<CelestialRuntime> Celestials,
|
|
Dictionary<int, Dictionary<string, CelestialRuntime>> LagrangeNodesByPlanetIndex);
|
|
|
|
internal sealed record LagrangePointPlacement(string Designation, Vector3 Position);
|
|
|
|
internal sealed record StationPlacement(AnchorRuntime Anchor, CelestialRuntime? Celestial, Vector3 Position);
|