487 lines
17 KiB
C#
487 lines
17 KiB
C#
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
|
|
|
namespace SpaceGame.Api.Universe.Scenario;
|
|
|
|
internal sealed class SystemGenerationService
|
|
{
|
|
private const string SolSystemId = "sol";
|
|
private const string DevelopmentCompanionSystemId = "helios";
|
|
|
|
internal List<SolarSystemDefinition> InjectSpecialSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
|
|
authoredSystems
|
|
.Select(CloneSystemDefinition)
|
|
.ToList();
|
|
|
|
internal List<SolarSystemDefinition> ExpandSystems(
|
|
IReadOnlyList<SolarSystemDefinition> authoredSystems,
|
|
int targetSystemCount)
|
|
{
|
|
var systems = authoredSystems
|
|
.Select(CloneSystemDefinition)
|
|
.ToList();
|
|
|
|
if (targetSystemCount <= 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
if (systems.Count > targetSystemCount)
|
|
{
|
|
return TrimSystemsToTarget(systems, targetSystemCount);
|
|
}
|
|
|
|
if (systems.Count >= targetSystemCount || authoredSystems.Count == 0)
|
|
{
|
|
return systems;
|
|
}
|
|
|
|
var existingIds = systems
|
|
.Select(system => system.Id)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
var generatedPositions = BuildGalaxyPositions(
|
|
authoredSystems.Select(system => ToVector(system.Position)).ToList(),
|
|
targetSystemCount - systems.Count);
|
|
|
|
for (var index = systems.Count; index < targetSystemCount; index += 1)
|
|
{
|
|
var template = authoredSystems[index % authoredSystems.Count];
|
|
var name = GeneratedSystemNames[(index - authoredSystems.Count) % GeneratedSystemNames.Length];
|
|
var id = BuildGeneratedSystemId(name, index + 1);
|
|
while (!existingIds.Add(id))
|
|
{
|
|
id = $"{id}-x";
|
|
}
|
|
|
|
systems.Add(CreateGeneratedSystem(template, name, id, index - authoredSystems.Count, generatedPositions[index - authoredSystems.Count]));
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
AddById(SolSystemId);
|
|
AddById(DevelopmentCompanionSystemId);
|
|
|
|
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,
|
|
string id,
|
|
int generatedIndex,
|
|
Vector3 position)
|
|
{
|
|
var starProfile = SelectStarProfile(generatedIndex);
|
|
var planets = BuildGeneratedPlanets(template, generatedIndex);
|
|
var resourceNodes = BuildProceduralResourceNodes(template, planets, generatedIndex)
|
|
.Select(node => new ResourceNodeDefinition
|
|
{
|
|
SourceKind = node.SourceKind,
|
|
Angle = node.Angle,
|
|
RadiusOffset = node.RadiusOffset,
|
|
InclinationDegrees = node.InclinationDegrees,
|
|
AnchorPlanetIndex = node.AnchorPlanetIndex,
|
|
AnchorMoonIndex = node.AnchorMoonIndex,
|
|
OreAmount = node.OreAmount,
|
|
ItemId = node.ItemId,
|
|
ShardCount = node.ShardCount,
|
|
})
|
|
.ToList();
|
|
|
|
return new SolarSystemDefinition
|
|
{
|
|
Id = id,
|
|
Label = label,
|
|
Position = [position.X, position.Y, position.Z],
|
|
Stars =
|
|
[
|
|
new StarDefinition
|
|
{
|
|
Kind = starProfile.Kind,
|
|
Color = starProfile.StarColor,
|
|
Glow = starProfile.StarGlow,
|
|
Size = starProfile.BaseSize + ((generatedIndex % 4) * 2f),
|
|
},
|
|
],
|
|
AsteroidField = new AsteroidFieldDefinition
|
|
{
|
|
DecorationCount = template.AsteroidField.DecorationCount + ((generatedIndex % 5) * 10),
|
|
RadiusOffset = template.AsteroidField.RadiusOffset + ((generatedIndex % 4) * 18000f),
|
|
RadiusVariance = template.AsteroidField.RadiusVariance + ((generatedIndex % 3) * 12000f),
|
|
HeightVariance = template.AsteroidField.HeightVariance + ((generatedIndex % 4) * 4000f),
|
|
},
|
|
ResourceNodes = resourceNodes,
|
|
Planets = planets,
|
|
};
|
|
}
|
|
|
|
private static SolarSystemDefinition CloneSystemDefinition(SolarSystemDefinition definition)
|
|
{
|
|
return new SolarSystemDefinition
|
|
{
|
|
Id = definition.Id,
|
|
Label = definition.Label,
|
|
Position = definition.Position.ToArray(),
|
|
Stars = definition.Stars.Select(s => new StarDefinition { Kind = s.Kind, Color = s.Color, Glow = s.Glow, Size = s.Size, OrbitRadius = s.OrbitRadius, OrbitSpeed = s.OrbitSpeed, OrbitPhaseAtEpoch = s.OrbitPhaseAtEpoch }).ToList(),
|
|
AsteroidField = new AsteroidFieldDefinition
|
|
{
|
|
DecorationCount = definition.AsteroidField.DecorationCount,
|
|
RadiusOffset = definition.AsteroidField.RadiusOffset,
|
|
RadiusVariance = definition.AsteroidField.RadiusVariance,
|
|
HeightVariance = definition.AsteroidField.HeightVariance,
|
|
},
|
|
ResourceNodes = definition.ResourceNodes.Select(node => new ResourceNodeDefinition
|
|
{
|
|
SourceKind = node.SourceKind,
|
|
Angle = node.Angle,
|
|
RadiusOffset = node.RadiusOffset,
|
|
InclinationDegrees = node.InclinationDegrees,
|
|
AnchorPlanetIndex = node.AnchorPlanetIndex,
|
|
AnchorMoonIndex = node.AnchorMoonIndex,
|
|
OreAmount = node.OreAmount,
|
|
ItemId = node.ItemId,
|
|
ShardCount = node.ShardCount,
|
|
}).ToList(),
|
|
Planets = definition.Planets.Select(planet => new PlanetDefinition
|
|
{
|
|
Label = planet.Label,
|
|
PlanetType = planet.PlanetType,
|
|
Shape = planet.Shape,
|
|
Moons = planet.Moons.Select(moon => new MoonDefinition { Label = moon.Label, Size = moon.Size, Color = moon.Color, OrbitRadius = moon.OrbitRadius, OrbitSpeed = moon.OrbitSpeed, OrbitPhaseAtEpoch = moon.OrbitPhaseAtEpoch, OrbitInclination = moon.OrbitInclination, OrbitLongitudeOfAscendingNode = moon.OrbitLongitudeOfAscendingNode }).ToList(),
|
|
OrbitRadius = planet.OrbitRadius,
|
|
OrbitSpeed = planet.OrbitSpeed,
|
|
OrbitEccentricity = planet.OrbitEccentricity,
|
|
OrbitInclination = planet.OrbitInclination,
|
|
OrbitLongitudeOfAscendingNode = planet.OrbitLongitudeOfAscendingNode,
|
|
OrbitArgumentOfPeriapsis = planet.OrbitArgumentOfPeriapsis,
|
|
OrbitPhaseAtEpoch = planet.OrbitPhaseAtEpoch,
|
|
Size = planet.Size,
|
|
Color = planet.Color,
|
|
Tilt = planet.Tilt,
|
|
HasRing = planet.HasRing,
|
|
}).ToList(),
|
|
};
|
|
}
|
|
|
|
private static List<ResourceNodeDefinition> BuildProceduralResourceNodes(
|
|
SolarSystemDefinition template,
|
|
IReadOnlyList<PlanetDefinition> planets,
|
|
int generatedIndex)
|
|
{
|
|
var nodes = new List<ResourceNodeDefinition>();
|
|
if (template.ResourceNodes.Count > 0)
|
|
{
|
|
nodes.AddRange(template.ResourceNodes.Select(node => new ResourceNodeDefinition
|
|
{
|
|
SourceKind = node.SourceKind,
|
|
Angle = node.Angle,
|
|
RadiusOffset = node.RadiusOffset,
|
|
InclinationDegrees = node.InclinationDegrees,
|
|
AnchorPlanetIndex = node.AnchorPlanetIndex,
|
|
AnchorMoonIndex = node.AnchorMoonIndex,
|
|
OreAmount = node.OreAmount,
|
|
ItemId = node.ItemId,
|
|
ShardCount = node.ShardCount,
|
|
}));
|
|
}
|
|
|
|
nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets));
|
|
return nodes;
|
|
}
|
|
|
|
private static List<Vector3> BuildGalaxyPositions(IReadOnlyCollection<Vector3> occupiedPositions, int count)
|
|
{
|
|
var allPositions = occupiedPositions.ToList();
|
|
var generated = new List<Vector3>(count);
|
|
|
|
for (var index = 0; index < count; index += 1)
|
|
{
|
|
Vector3? accepted = null;
|
|
for (var attempt = 0; attempt < 64; attempt += 1)
|
|
{
|
|
var candidate = ComputeGeneratedSystemPosition(index, attempt);
|
|
if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
|
|
{
|
|
accepted = candidate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
accepted ??= ComputeFallbackGeneratedSystemPosition(index);
|
|
generated.Add(accepted.Value);
|
|
allPositions.Add(accepted.Value);
|
|
}
|
|
|
|
return generated;
|
|
}
|
|
|
|
private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt)
|
|
{
|
|
const int armCount = 4;
|
|
const float baseInnerRadius = 9f;
|
|
const float radiusStep = 0.54f;
|
|
const float armOffset = MathF.PI * 2f / armCount;
|
|
|
|
var armIndex = (generatedIndex + attempt) % armCount;
|
|
var armDepth = generatedIndex / armCount;
|
|
var radius = baseInnerRadius + (armDepth * radiusStep) + Jitter(generatedIndex * 17 + attempt, 0, 0.9f);
|
|
var angle = (armIndex * armOffset) + (radius / 8.2f) + Jitter(generatedIndex, 1 + attempt, 0.16f);
|
|
var x = MathF.Cos(angle) * radius;
|
|
var z = MathF.Sin(angle) * radius * 0.58f;
|
|
var y = ComputeSystemHeight(radius, generatedIndex, attempt);
|
|
return new Vector3(x, y, z);
|
|
}
|
|
|
|
private static Vector3 ComputeFallbackGeneratedSystemPosition(int generatedIndex)
|
|
{
|
|
const int ringCount = 5;
|
|
const float fallbackRadius = 42f;
|
|
var angle = (generatedIndex % ringCount) * (MathF.PI * 2f / ringCount) + (generatedIndex / ringCount) * 0.22f;
|
|
var radius = fallbackRadius + (generatedIndex / ringCount) * 1.8f;
|
|
return new Vector3(
|
|
MathF.Cos(angle) * radius,
|
|
ComputeSystemHeight(radius, generatedIndex, 99),
|
|
MathF.Sin(angle) * radius * 0.6f);
|
|
}
|
|
|
|
private static string BuildGeneratedSystemId(string label, int ordinal)
|
|
{
|
|
var slug = string.Concat(label
|
|
.ToLowerInvariant()
|
|
.Select(character => char.IsLetterOrDigit(character) ? character : '-'))
|
|
.Trim('-');
|
|
|
|
return $"gen-{ordinal}-{slug}";
|
|
}
|
|
|
|
private static IEnumerable<ResourceNodeDefinition> BuildAsteroidBeltNodes(int generatedIndex, IReadOnlyList<PlanetDefinition> planets)
|
|
{
|
|
var nodeCount = 4 + (generatedIndex % 4);
|
|
var oreAmount = 1000f;
|
|
|
|
for (var index = 0; index < nodeCount; index += 1)
|
|
{
|
|
yield return new ResourceNodeDefinition
|
|
{
|
|
SourceKind = "asteroid-belt",
|
|
Angle = ((MathF.PI * 2f) / nodeCount) * index + Jitter(generatedIndex, 180 + index, 0.22f),
|
|
RadiusOffset = 120000f + Jitter(generatedIndex, 200 + index, 36000f),
|
|
InclinationDegrees = Jitter(generatedIndex, 280 + index, 12f),
|
|
AnchorPlanetIndex = ResolveAsteroidAnchorPlanetIndex(planets),
|
|
OreAmount = oreAmount,
|
|
ItemId = "ore",
|
|
ShardCount = 6 + (index % 4),
|
|
};
|
|
}
|
|
}
|
|
|
|
private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList<PlanetDefinition> planets)
|
|
{
|
|
if (planets.Count == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var gasGiantIndex = -1;
|
|
for (var index = 0; index < planets.Count; index += 1)
|
|
{
|
|
if (planets[index].PlanetType is "gas-giant" or "ice-giant")
|
|
{
|
|
gasGiantIndex = index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (gasGiantIndex > 0)
|
|
{
|
|
return gasGiantIndex - 1;
|
|
}
|
|
|
|
return Math.Clamp((planets.Count / 2) - 1, 0, planets.Count - 1);
|
|
}
|
|
|
|
private static List<PlanetDefinition> BuildGeneratedPlanets(SolarSystemDefinition template, int generatedIndex)
|
|
{
|
|
var planetCount = 2 + (int)MathF.Floor(Hash01(generatedIndex, 2) * 7f);
|
|
var planets = new List<PlanetDefinition>(planetCount);
|
|
var orbitRadius = 0.24f + (Hash01(generatedIndex, 3) * 0.12f);
|
|
var sourcePlanets = template.Planets.Count > 0 ? template.Planets : null;
|
|
|
|
for (var index = 0; index < planetCount; index += 1)
|
|
{
|
|
var profile = SelectPlanetProfile(generatedIndex, index);
|
|
var templatePlanet = sourcePlanets is not null && sourcePlanets.Count > 0
|
|
? sourcePlanets[index % sourcePlanets.Count]
|
|
: null;
|
|
|
|
orbitRadius += profile.OrbitGapMin + (Hash01(generatedIndex, 10 + index) * (profile.OrbitGapMax - profile.OrbitGapMin));
|
|
var orbitEccentricity = 0.01f + (Hash01(generatedIndex, 20 + index) * 0.16f);
|
|
var orbitInclination = -9f + (Hash01(generatedIndex, 30 + index) * 18f);
|
|
var moonCount = profile.BaseMoonCount + (int)MathF.Floor(Hash01(generatedIndex, 40 + index) * 3f);
|
|
var planetLabel = $"{BuildPlanetBaseName(generatedIndex, index)}-{index + 1}";
|
|
|
|
planets.Add(new PlanetDefinition
|
|
{
|
|
Label = planetLabel,
|
|
PlanetType = profile.Type,
|
|
Shape = profile.Shape,
|
|
Moons = GenerateMoons(planetLabel, profile.BaseSize, moonCount),
|
|
OrbitRadius = orbitRadius,
|
|
OrbitSpeed = 0.11f / MathF.Sqrt(MathF.Max(orbitRadius * orbitRadius * orbitRadius, 0.02f)),
|
|
OrbitEccentricity = orbitEccentricity,
|
|
OrbitInclination = orbitInclination,
|
|
OrbitLongitudeOfAscendingNode = Hash01(generatedIndex, 120 + index) * 360f,
|
|
OrbitArgumentOfPeriapsis = Hash01(generatedIndex, 140 + index) * 360f,
|
|
OrbitPhaseAtEpoch = Hash01(generatedIndex, 160 + index) * 360f,
|
|
Size = profile.BaseSize + (Hash01(generatedIndex, 50 + index) * (profile.BaseSize * 0.35f)),
|
|
Color = templatePlanet?.Color ?? profile.Color,
|
|
Tilt = -0.45f + (Hash01(generatedIndex, 60 + index) * 0.9f),
|
|
HasRing = profile.CanHaveRing && Hash01(generatedIndex, 70 + index) > 0.55f,
|
|
});
|
|
}
|
|
|
|
return planets;
|
|
}
|
|
|
|
private static StarProfile SelectStarProfile(int generatedIndex)
|
|
{
|
|
var value = Hash01(generatedIndex, 80);
|
|
return value switch
|
|
{
|
|
< 0.32f => StarProfiles[0],
|
|
< 0.54f => StarProfiles[1],
|
|
< 0.68f => StarProfiles[5],
|
|
< 0.8f => StarProfiles[2],
|
|
< 0.9f => StarProfiles[3],
|
|
< 0.97f => StarProfiles[6],
|
|
_ => StarProfiles[4],
|
|
};
|
|
}
|
|
|
|
private static PlanetProfile SelectPlanetProfile(int generatedIndex, int planetIndex)
|
|
{
|
|
var value = Hash01(generatedIndex, 90 + planetIndex);
|
|
return value switch
|
|
{
|
|
< 0.14f => PlanetProfiles[7],
|
|
< 0.28f => PlanetProfiles[0],
|
|
< 0.46f => PlanetProfiles[3],
|
|
< 0.62f => PlanetProfiles[1],
|
|
< 0.74f => PlanetProfiles[2],
|
|
< 0.86f => PlanetProfiles[4],
|
|
< 0.94f => PlanetProfiles[6],
|
|
_ => PlanetProfiles[5],
|
|
};
|
|
}
|
|
|
|
private static string BuildPlanetBaseName(int generatedIndex, int planetIndex)
|
|
{
|
|
var source = GeneratedSystemNames[generatedIndex % GeneratedSystemNames.Length]
|
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)[0];
|
|
return source[..Math.Min(source.Length, 6)];
|
|
}
|
|
|
|
private static float ComputeSystemHeight(float radius, int generatedIndex, int salt)
|
|
{
|
|
var normalized = MathF.Min(1f, MathF.Max(0f, (radius - 8f) / 28f));
|
|
var band = 0.22f + (normalized * 0.76f);
|
|
return (Hash01(generatedIndex, 100 + salt) * 2f - 1f) * band;
|
|
}
|
|
|
|
private static float Jitter(int index, int salt, float amplitude) =>
|
|
(Hash01(index, salt) * 2f - 1f) * amplitude;
|
|
|
|
private static float Hash01(int index, int salt)
|
|
{
|
|
uint value = (uint)(index + 1);
|
|
value ^= (uint)(salt + 0x9e3779b9);
|
|
value *= 0x85ebca6b;
|
|
value ^= value >> 13;
|
|
value *= 0xc2b2ae35;
|
|
value ^= value >> 16;
|
|
return (value & 0x00ffffff) / 16777215f;
|
|
}
|
|
|
|
private static List<MoonDefinition> GenerateMoons(string planetLabel, float planetSize, int moonCount)
|
|
{
|
|
var seed = planetLabel.Aggregate(0, (acc, c) => acc * 31 + c);
|
|
var moons = new List<MoonDefinition>(moonCount);
|
|
for (var moonIndex = 0; moonIndex < moonCount; moonIndex += 1)
|
|
{
|
|
var spacing = planetSize * 1.4f;
|
|
var radiusVariance = Hash01(seed, 10 + moonIndex) * planetSize * 0.9f;
|
|
var orbitRadius = (planetSize * 1.8f) + (moonIndex * spacing) + radiusVariance;
|
|
var orbitSpeed = 0.9f / MathF.Sqrt(MathF.Max(orbitRadius, 1f)) + (moonIndex * 0.003f);
|
|
var phase = Hash01(seed, 20 + moonIndex) * 360f;
|
|
var inclination = (Hash01(seed, 30 + moonIndex) - 0.5f) * 28f;
|
|
var ascendingNode = Hash01(seed, 40 + moonIndex) * 360f;
|
|
var sizeBase = MathF.Max(2.2f, planetSize * 0.11f);
|
|
var sizeVariance = Hash01(seed, 50 + moonIndex) * MathF.Max(planetSize * 0.16f, 2.5f);
|
|
var size = MathF.Min(sizeBase + sizeVariance, planetSize * 0.42f);
|
|
|
|
moons.Add(new MoonDefinition
|
|
{
|
|
Label = $"{planetLabel}-m{moonIndex + 1}",
|
|
Size = size,
|
|
Color = "#c8c4bc",
|
|
OrbitRadius = orbitRadius,
|
|
OrbitSpeed = orbitSpeed,
|
|
OrbitPhaseAtEpoch = phase,
|
|
OrbitInclination = inclination,
|
|
OrbitLongitudeOfAscendingNode = ascendingNode,
|
|
});
|
|
}
|
|
|
|
return moons;
|
|
}
|
|
}
|