Refactor world bootstrap and allow empty startup worlds
This commit is contained in:
@@ -4,114 +4,65 @@ namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class SystemGenerationService
|
||||
{
|
||||
internal List<SolarSystemDefinition> PrepareAuthoredSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
|
||||
authoredSystems
|
||||
private const float KnownSystemSelectionChance = 0.5f;
|
||||
|
||||
internal List<SolarSystemDefinition> PrepareKnownSystems(IReadOnlyList<SolarSystemDefinition> knownSystems) =>
|
||||
knownSystems
|
||||
.Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
|
||||
.ToList();
|
||||
|
||||
internal List<SolarSystemDefinition> ExpandSystems(
|
||||
IReadOnlyList<SolarSystemDefinition> authoredSystems,
|
||||
int targetSystemCount)
|
||||
internal List<SolarSystemDefinition> GenerateSystems(
|
||||
IReadOnlyList<SolarSystemDefinition> knownSystems,
|
||||
WorldGenerationOptions worldGenerationOptions)
|
||||
{
|
||||
var systems = authoredSystems
|
||||
.Select(CloneSystemDefinition)
|
||||
.ToList();
|
||||
|
||||
if (targetSystemCount <= 0)
|
||||
if (worldGenerationOptions.TargetSystemCount <= 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (systems.Count > targetSystemCount)
|
||||
if (knownSystems.Count == 0)
|
||||
{
|
||||
return TrimSystemsToTarget(systems, targetSystemCount);
|
||||
throw new InvalidOperationException("World generation requires at least one known system template.");
|
||||
}
|
||||
|
||||
if (systems.Count >= targetSystemCount || authoredSystems.Count == 0)
|
||||
{
|
||||
return systems;
|
||||
}
|
||||
var systems = new List<SolarSystemDefinition>(worldGenerationOptions.TargetSystemCount);
|
||||
var availableKnownSystems = knownSystems.Select(CloneSystemDefinition).ToList();
|
||||
var templateSystems = knownSystems.Select(CloneSystemDefinition).ToList();
|
||||
|
||||
var existingIds = systems
|
||||
.Select(system => system.Id)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var generatedPositions = BuildGalaxyPositions(
|
||||
authoredSystems.Select(system => ToVector(system.Position)).ToList(),
|
||||
targetSystemCount - systems.Count);
|
||||
var existingIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
var occupiedPositions = new List<Vector3>();
|
||||
var generatedSystemCount = 0;
|
||||
|
||||
for (var index = systems.Count; index < targetSystemCount; index += 1)
|
||||
for (var slotIndex = 0; slotIndex < worldGenerationOptions.TargetSystemCount; slotIndex += 1)
|
||||
{
|
||||
var template = authoredSystems[index % authoredSystems.Count];
|
||||
var name = GeneratedSystemNames[(index - authoredSystems.Count) % GeneratedSystemNames.Length];
|
||||
var id = BuildGeneratedSystemId(name, index + 1);
|
||||
if (ShouldUseKnownSystem(worldGenerationOptions, slotIndex, availableKnownSystems.Count))
|
||||
{
|
||||
var knownSystemIndex = SelectKnownSystemIndex(worldGenerationOptions.Seed, slotIndex, availableKnownSystems.Count);
|
||||
var knownSystem = availableKnownSystems[knownSystemIndex];
|
||||
availableKnownSystems.RemoveAt(knownSystemIndex);
|
||||
systems.Add(knownSystem);
|
||||
existingIds.Add(knownSystem.Id);
|
||||
occupiedPositions.Add(ToVector(knownSystem.Position));
|
||||
continue;
|
||||
}
|
||||
|
||||
var template = templateSystems[generatedSystemCount % templateSystems.Count];
|
||||
var name = GeneratedSystemNames[generatedSystemCount % GeneratedSystemNames.Length];
|
||||
var id = BuildGeneratedSystemId(name, generatedSystemCount + 1);
|
||||
while (!existingIds.Add(id))
|
||||
{
|
||||
id = $"{id}-x";
|
||||
}
|
||||
|
||||
systems.Add(CreateGeneratedSystem(template, name, id, index - authoredSystems.Count, generatedPositions[index - authoredSystems.Count]));
|
||||
var position = BuildGeneratedSystemPosition(occupiedPositions, generatedSystemCount);
|
||||
systems.Add(CreateGeneratedSystem(template, name, id, generatedSystemCount, position));
|
||||
occupiedPositions.Add(position);
|
||||
generatedSystemCount += 1;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var preferredSystemId in SystemSelectionPolicy.PreferredSystemIds)
|
||||
{
|
||||
AddById(preferredSystemId);
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -260,30 +211,38 @@ public sealed class SystemGenerationService
|
||||
return system;
|
||||
}
|
||||
|
||||
private static List<Vector3> BuildGalaxyPositions(IReadOnlyCollection<Vector3> occupiedPositions, int count)
|
||||
private static Vector3 BuildGeneratedSystemPosition(IReadOnlyCollection<Vector3> occupiedPositions, int generatedIndex)
|
||||
{
|
||||
var allPositions = occupiedPositions.ToList();
|
||||
var generated = new List<Vector3>(count);
|
||||
|
||||
for (var index = 0; index < count; index += 1)
|
||||
for (var attempt = 0; attempt < 64; attempt += 1)
|
||||
{
|
||||
Vector3? accepted = null;
|
||||
for (var attempt = 0; attempt < 64; attempt += 1)
|
||||
var candidate = ComputeGeneratedSystemPosition(generatedIndex, attempt);
|
||||
if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
|
||||
{
|
||||
var candidate = ComputeGeneratedSystemPosition(index, attempt);
|
||||
if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
|
||||
{
|
||||
accepted = candidate;
|
||||
break;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
accepted ??= ComputeFallbackGeneratedSystemPosition(index);
|
||||
generated.Add(accepted.Value);
|
||||
allPositions.Add(accepted.Value);
|
||||
}
|
||||
|
||||
return generated;
|
||||
return ComputeFallbackGeneratedSystemPosition(generatedIndex);
|
||||
}
|
||||
|
||||
private static bool ShouldUseKnownSystem(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
int slotIndex,
|
||||
int remainingKnownSystemCount)
|
||||
{
|
||||
if (!worldGenerationOptions.UseKnownSystems || remainingKnownSystemCount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Hash01(worldGenerationOptions.Seed, 700 + slotIndex) >= KnownSystemSelectionChance;
|
||||
}
|
||||
|
||||
private static int SelectKnownSystemIndex(int seed, int slotIndex, int remainingKnownSystemCount)
|
||||
{
|
||||
var selection = Hash01(seed, 900 + slotIndex);
|
||||
return Math.Min((int)(selection * remainingKnownSystemCount), remainingKnownSystemCount - 1);
|
||||
}
|
||||
|
||||
private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt)
|
||||
|
||||
Reference in New Issue
Block a user