feat: improved visualisation and x4 data import

This commit is contained in:
2026-03-18 20:58:17 -04:00
parent 358122a74a
commit f98c47a8a7
45 changed files with 32840 additions and 1482 deletions

View File

@@ -1,20 +1,36 @@
namespace SpaceGame.Simulation.Api.Contracts;
public sealed record StarSnapshot(
string Kind,
string Color,
string Glow,
float Size,
float OrbitRadius,
float OrbitSpeed,
float OrbitPhaseAtEpoch);
public sealed record MoonSnapshot(
string Label,
float Size,
string Color,
float OrbitRadius,
float OrbitSpeed,
float OrbitPhaseAtEpoch,
float OrbitInclination,
float OrbitLongitudeOfAscendingNode);
public sealed record SystemSnapshot(
string Id,
string Label,
Vector3Dto GalaxyPosition,
string StarKind,
int StarCount,
string StarColor,
float StarSize,
IReadOnlyList<StarSnapshot> Stars,
IReadOnlyList<PlanetSnapshot> Planets);
public sealed record PlanetSnapshot(
string Label,
string PlanetType,
string Shape,
int MoonCount,
IReadOnlyList<MoonSnapshot> Moons,
float OrbitRadius,
float OrbitSpeed,
float OrbitEccentricity,

View File

@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace SpaceGame.Simulation.Api.Data;
public sealed class ConstructionDefinition
@@ -13,6 +15,29 @@ public sealed class ConstructionDefinition
public int Priority { get; set; }
}
public sealed class ItemPriceDefinition
{
public float Min { get; set; }
public float Max { get; set; }
public float Avg { get; set; }
}
public sealed class ItemEffectDefinition
{
public required string Type { get; set; }
public float Product { get; set; }
}
public sealed class ItemProductionDefinition
{
public float Time { get; set; }
public float Amount { get; set; }
public string Method { get; set; } = "default";
public string Name { get; set; } = "Universal";
public List<RecipeInputDefinition> Wares { get; set; } = [];
public List<ItemEffectDefinition> Effects { get; set; } = [];
}
public sealed class BalanceDefinition
{
public float YPlane { get; set; }
@@ -25,17 +50,35 @@ public sealed class BalanceDefinition
public float UndockDistance { get; set; }
}
public sealed class StarDefinition
{
public string Kind { get; set; } = "main-sequence";
public required string Color { get; set; }
public required string Glow { get; set; }
public float Size { get; set; }
public float OrbitRadius { get; set; }
public float OrbitSpeed { get; set; }
public float OrbitPhaseAtEpoch { get; set; }
}
public sealed class MoonDefinition
{
public required string Label { get; set; }
public float Size { get; set; }
public required string Color { get; set; }
public float OrbitRadius { get; set; }
public float OrbitSpeed { get; set; }
public float OrbitPhaseAtEpoch { get; set; }
public float OrbitInclination { get; set; }
public float OrbitLongitudeOfAscendingNode { get; set; }
}
public sealed class SolarSystemDefinition
{
public required string Id { get; set; }
public required string Label { get; set; }
public required float[] Position { get; set; }
public string StarKind { get; set; } = "main-sequence";
public int StarCount { get; set; } = 1;
public required string StarColor { get; set; }
public required string StarGlow { get; set; }
public float StarSize { get; set; }
public float GravityWellRadius { get; set; }
public required List<StarDefinition> Stars { get; set; }
public required AsteroidFieldDefinition AsteroidField { get; set; }
public required List<ResourceNodeDefinition> ResourceNodes { get; set; }
public required List<PlanetDefinition> Planets { get; set; }
@@ -68,9 +111,21 @@ public sealed class ItemDefinition
public required string Name { get; set; }
public string Description { get; set; } = string.Empty;
public string Type { get; set; } = "material";
public required string CargoKind { get; set; }
public string CargoKind { get; set; } = string.Empty;
public float Volume { get; set; } = 1f;
public int Version { get; set; }
public string FactoryName { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
public string Group { get; set; } = string.Empty;
public ItemPriceDefinition? Price { get; set; }
public List<string> Illegal { get; set; } = [];
public List<ItemProductionDefinition> Production { get; set; } = [];
public ConstructionDefinition? Construction { get; set; }
[JsonPropertyName("transport")]
public string Transport
{
set => CargoKind = value;
}
}
public sealed class RecipeOutputDefinition
@@ -81,8 +136,13 @@ public sealed class RecipeOutputDefinition
public sealed class RecipeInputDefinition
{
public required string ItemId { get; set; }
public string ItemId { get; set; } = string.Empty;
public float Amount { get; set; }
[JsonPropertyName("ware")]
public string Ware
{
set => ItemId = value;
}
}
public sealed class ModuleConstructionDefinition
@@ -91,18 +151,73 @@ public sealed class ModuleConstructionDefinition
public float ProductionTime { get; set; }
}
public sealed class ModuleDockDefinition
{
public int Capacity { get; set; }
public required string Size { get; set; }
}
public sealed class ModuleCargoDefinition
{
public float Max { get; set; }
public required string Type { get; set; }
}
public sealed class ModuleWorkForceDefinition
{
public float Capacity { get; set; }
public float Max { get; set; }
public string Race { get; set; } = string.Empty;
}
public sealed class ModuleMountDefinition
{
public required string Group { get; set; }
public required string Size { get; set; }
public bool Hittable { get; set; }
public List<string> Types { get; set; } = [];
}
public sealed class ModuleProductionDefinition
{
public float Time { get; set; }
public float Amount { get; set; }
public string Method { get; set; } = "default";
public string Name { get; set; } = "Universal";
public List<RecipeInputDefinition> Wares { get; set; } = [];
}
public sealed class ModuleDefinition
{
public required string Id { get; set; }
public required string Name { get; set; }
public string Description { get; set; } = string.Empty;
public required string Type { get; set; }
[JsonIgnore]
public string? Product { get; set; }
public List<string> Products { get; set; } = [];
public string ProductionMode { get; set; } = "passive";
public float Radius { get; set; } = 12f;
public float Hull { get; set; } = 100f;
public float WorkforceNeeded { get; set; }
public int Version { get; set; }
public string Macro { get; set; } = string.Empty;
public string MakerRace { get; set; } = string.Empty;
public int ExplosionDamage { get; set; }
public ItemPriceDefinition? Price { get; set; }
public List<string> Owners { get; set; } = [];
public ModuleCargoDefinition? Cargo { get; set; }
public ModuleWorkForceDefinition? WorkForce { get; set; }
public List<ModuleDockDefinition> Docks { get; set; } = [];
public List<ModuleMountDefinition> Shields { get; set; } = [];
public List<ModuleMountDefinition> Turrets { get; set; } = [];
public List<ModuleProductionDefinition> Production { get; set; } = [];
public ModuleConstructionDefinition? Construction { get; set; }
[JsonPropertyName("product")]
public List<string> ProductIds
{
set => Products = value ?? [];
}
}
public sealed class ModuleRecipeDefinition
@@ -130,7 +245,7 @@ public sealed class PlanetDefinition
public required string Label { get; set; }
public string PlanetType { get; set; } = "terrestrial";
public string Shape { get; set; } = "sphere";
public int MoonCount { get; set; }
public List<MoonDefinition> Moons { get; set; } = [];
public float OrbitRadius { get; set; }
public float OrbitSpeed { get; set; }
public float OrbitEccentricity { get; set; }

View File

@@ -8,19 +8,11 @@ public sealed partial class ScenarioLoader
private const string DevelopmentCompanionSystemId = "helios";
private static List<SolarSystemDefinition> InjectSpecialSystems(
IReadOnlyList<SolarSystemDefinition> authoredSystems,
bool includeSolSystem)
IReadOnlyList<SolarSystemDefinition> authoredSystems)
{
var systems = authoredSystems
return authoredSystems
.Select(CloneSystemDefinition)
.ToList();
if (includeSolSystem && systems.All((system) => system.Id != "sol"))
{
systems.Add(CreateSolSystem());
}
return systems;
}
private static List<SolarSystemDefinition> ExpandSystems(
@@ -156,12 +148,16 @@ public sealed partial class ScenarioLoader
Id = id,
Label = label,
Position = [position.X, position.Y, position.Z],
StarKind = starProfile.Kind,
StarCount = starProfile.StarCount,
StarColor = starProfile.StarColor,
StarGlow = starProfile.StarGlow,
StarSize = starProfile.BaseSize + ((generatedIndex % 4) * 2f),
GravityWellRadius = template.GravityWellRadius + ((generatedIndex % 3) * 12f),
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),
@@ -181,12 +177,7 @@ public sealed partial class ScenarioLoader
Id = definition.Id,
Label = definition.Label,
Position = definition.Position.ToArray(),
StarKind = definition.StarKind,
StarCount = definition.StarCount,
StarColor = definition.StarColor,
StarGlow = definition.StarGlow,
StarSize = definition.StarSize,
GravityWellRadius = definition.GravityWellRadius,
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,
@@ -214,7 +205,7 @@ public sealed partial class ScenarioLoader
Label = planet.Label,
PlanetType = planet.PlanetType,
Shape = planet.Shape,
MoonCount = planet.MoonCount,
Moons = planet.Moons.Select(m => new MoonDefinition { Label = m.Label, Size = m.Size, Color = m.Color, OrbitRadius = m.OrbitRadius, OrbitSpeed = m.OrbitSpeed, OrbitPhaseAtEpoch = m.OrbitPhaseAtEpoch, OrbitInclination = m.OrbitInclination, OrbitLongitudeOfAscendingNode = m.OrbitLongitudeOfAscendingNode }).ToList(),
OrbitRadius = planet.OrbitRadius,
OrbitSpeed = planet.OrbitSpeed,
OrbitEccentricity = planet.OrbitEccentricity,
@@ -387,14 +378,15 @@ public sealed partial class ScenarioLoader
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 moonVariance = (int)MathF.Floor(Hash01(generatedIndex, 40 + index) * 3f);
var moonCount = profile.BaseMoonCount + (int)MathF.Floor(Hash01(generatedIndex, 40 + index) * 3f);
var planetLabel = $"{BuildPlanetBaseName(generatedIndex, index)}-{index + 1}";
planets.Add(new PlanetDefinition
{
Label = $"{BuildPlanetBaseName(generatedIndex, index)}-{index + 1}",
Label = planetLabel,
PlanetType = profile.Type,
Shape = profile.Shape,
MoonCount = profile.BaseMoonCount + moonVariance,
Moons = GenerateMoons(planetLabel, profile.BaseSize, moonCount),
OrbitRadius = orbitRadius,
OrbitSpeed = 0.11f / MathF.Sqrt(MathF.Max(orbitRadius * orbitRadius * orbitRadius, 0.02f)),
OrbitEccentricity = orbitEccentricity,
@@ -471,12 +463,44 @@ public sealed partial class ScenarioLoader
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;
}
private sealed record StarProfile(
string Kind,
string StarColor,
string StarGlow,
float BaseSize,
int StarCount);
float BaseSize);
private sealed record PlanetProfile(
string Type,
@@ -490,106 +514,4 @@ public sealed partial class ScenarioLoader
public float OrbitGapMax => OrbitGapMin + MathF.Max(0.12f, OrbitGapMin * 0.45f);
}
private static SolarSystemDefinition CreateSolSystem()
{
var mercuryOrbitAu = 0.3871f;
var venusOrbitAu = 0.7233f;
var earthOrbitAu = 1.000f;
var marsOrbitAu = 1.5237f;
var jupiterOrbitAu = 5.203f;
var saturnOrbitAu = 9.582f;
var uranusOrbitAu = 19.201f;
var neptuneOrbitAu = 30.047f;
return new SolarSystemDefinition
{
Id = "sol",
Label = "Sol",
Position = [18.2f, 0.02f, -11.8f],
StarKind = "main-sequence",
StarCount = 1,
StarColor = "#fff1b8",
StarGlow = "#ffd35a",
StarSize = 696340f,
GravityWellRadius = 240f,
AsteroidField = new AsteroidFieldDefinition
{
DecorationCount = 240,
RadiusOffset = 422000000f,
RadiusVariance = 180000000f,
HeightVariance = 22000000f,
},
ResourceNodes =
[
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 0.2f, RadiusOffset = 126000f, InclinationDegrees = 4f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 1.8f, RadiusOffset = 148000f, InclinationDegrees = -6f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 3.5f, RadiusOffset = 138000f, InclinationDegrees = 8f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 5.1f, RadiusOffset = 164000f, InclinationDegrees = -5f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
],
Planets =
[
CreateSolPlanet("Mercury", "barren", "sphere", 0, mercuryOrbitAu, 0.2056f, 7.0f, 48f, 29f, 252f, "#b7a08f", 0.03f, false),
CreateSolPlanet("Venus", "desert", "sphere", 0, venusOrbitAu, 0.0067f, 3.4f, 76f, 54f, 181f, "#d9b38c", 2.64f, false),
CreateSolPlanet("Earth", "terrestrial", "sphere", 1, earthOrbitAu, 0.0167f, 0.0f, 0f, 114f, 100f, "#4f84c4", 0.41f, false),
CreateSolPlanet("Mars", "desert", "sphere", 2, marsOrbitAu, 0.0934f, 1.85f, 49f, 286f, 54f, "#c56e52", 0.44f, false),
CreateSolPlanet("Jupiter", "gas-giant", "oblate", 95, jupiterOrbitAu, 0.0489f, 1.3f, 100f, 275f, 34f, "#d9b06f", 0.05f, true),
CreateSolPlanet("Saturn", "gas-giant", "oblate", 146, saturnOrbitAu, 0.0565f, 2.49f, 113f, 339f, 200f, "#dfc27d", 0.47f, true),
CreateSolPlanet("Uranus", "ice-giant", "oblate", 28, uranusOrbitAu, 0.046f, 0.77f, 74f, 97f, 130f, "#9fd3df", 1.71f, true),
CreateSolPlanet("Neptune", "ice-giant", "oblate", 16, neptuneOrbitAu, 0.009f, 1.77f, 132f, 273f, 256f, "#4c79c9", 0.49f, true)
],
};
}
private static PlanetDefinition CreateSolPlanet(
string label,
string planetType,
string shape,
int moonCount,
float orbitRadiusAu,
float orbitEccentricity,
float orbitInclination,
float ascendingNode,
float argumentOfPeriapsis,
float phaseAtEpoch,
string color,
float tilt,
bool hasRing)
{
return new PlanetDefinition
{
Label = label,
PlanetType = planetType,
Shape = shape,
MoonCount = moonCount,
OrbitRadius = orbitRadiusAu,
OrbitSpeed = ComputeSolOrbitSpeed(orbitRadiusAu),
OrbitEccentricity = orbitEccentricity,
OrbitInclination = orbitInclination,
OrbitLongitudeOfAscendingNode = ascendingNode,
OrbitArgumentOfPeriapsis = argumentOfPeriapsis,
OrbitPhaseAtEpoch = phaseAtEpoch,
Size = planetType switch
{
"gas-giant" => label == "Saturn" ? 58232f : 69911f,
"ice-giant" => label == "Uranus" ? 25362f : 24622f,
_ => label switch
{
"Mercury" => 2440f,
"Venus" => 6052f,
"Earth" => 6371f,
"Mars" => 3390f,
_ => 5000f,
},
},
Color = color,
Tilt = tilt,
HasRing = hasRing,
};
}
private static float ComputeSolOrbitSpeed(float orbitRadiusAu)
{
const float earthAngularSpeed = 0.11f;
return earthAngularSpeed / MathF.Sqrt(orbitRadiusAu * orbitRadiusAu * orbitRadiusAu);
}
}

View File

@@ -60,14 +60,14 @@ public sealed partial class ScenarioLoader
.ToList();
var refineries = ownedStations
.Where((station) => HasInstalledModules(station, "refinery-stack", "power-core", "liquid-tank"))
.Where((station) => HasInstalledModules(station, "module_gen_prod_refinedmetals_01", "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01"))
.ToList();
if (refineries.Count > 0)
{
foreach (var refinery in refineries)
{
refinery.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(refinery.Inventory, "refined-metals"), MinimumRefineryStock);
refinery.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(refinery.Inventory, "refinedmetals"), MinimumRefineryStock);
}
if (refineries.All((station) => GetInventoryAmount(station.Inventory, "ore") < MinimumRefineryOre))
@@ -76,9 +76,9 @@ public sealed partial class ScenarioLoader
}
}
foreach (var shipyard in ownedStations.Where((station) => HasInstalledModules(station, "ship-factory")))
foreach (var shipyard in ownedStations.Where((station) => HasInstalledModules(station, "module_gen_build_l_01")))
{
shipyard.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refined-metals"), MinimumShipyardStock);
shipyard.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refinedmetals"), MinimumShipyardStock);
}
}
}
@@ -189,13 +189,13 @@ public sealed partial class ScenarioLoader
{
foreach (var (moduleId, targetCount) in new (string ModuleId, int TargetCount)[]
{
("refinery-stack", 1),
("container-bay", 1),
("fabricator-array", 2),
("component-factory", 1),
("ship-factory", 1),
("solar-array", 2),
("dock-bay-small", 2),
("module_gen_prod_refinedmetals_01", 1),
("module_arg_stor_container_m_01", 1),
("module_gen_prod_hullparts_01", 2),
("module_gen_prod_advancedelectronics_01", 1),
("module_gen_build_l_01", 1),
("module_gen_prod_energycells_01", 2),
("module_arg_dock_m_01_lowtech", 2),
})
{
if (CountModules(station.InstalledModules, moduleId) < targetCount
@@ -210,7 +210,7 @@ public sealed partial class ScenarioLoader
private static void InitializeStationPopulation(StationRuntime station)
{
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01");
station.PopulationCapacity = 40f + (habitatModules * 220f);
station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
station.Population = habitatModules > 0

View File

@@ -9,13 +9,18 @@ public sealed partial class ScenarioLoader
var celestials = new List<CelestialRuntime>();
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();
AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-star",
systemId: system.Definition.Id,
kind: SpatialNodeKind.Star,
position: Vector3.Zero,
localSpaceRadius: MathF.Max(system.Definition.GravityWellRadius + StarBubbleRadiusPadding, LocalSpaceRadius));
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)
{
@@ -29,7 +34,7 @@ public sealed partial class ScenarioLoader
kind: SpatialNodeKind.Planet,
position: planetPosition,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: $"node-{system.Definition.Id}-star");
parentNodeId: primaryStarNodeId);
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
@@ -48,15 +53,10 @@ public sealed partial class ScenarioLoader
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
if (planet.MoonCount <= 0)
for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
{
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);
var moon = planet.Moons[moonIndex];
var moonPosition = ComputeMoonPosition(planetPosition, moon);
AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
@@ -65,7 +65,6 @@ public sealed partial class ScenarioLoader
position: moonPosition,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id);
moonOrbitRadius += 30f;
}
}
@@ -232,10 +231,11 @@ public sealed partial class ScenarioLoader
return new Vector3(x, 0f, z);
}
private static Vector3 ComputeMoonPosition(Vector3 planetPosition, float orbitRadius, int moonIndex, int planetIndex)
private static Vector3 ComputeMoonPosition(Vector3 planetPosition, MoonDefinition moon)
{
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));
var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch);
var local = new Vector3(MathF.Cos(angle) * moon.OrbitRadius, 0f, MathF.Sin(angle) * moon.OrbitRadius);
return Add(planetPosition, local);
}
private static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<CelestialRuntime> celestials)

View File

@@ -12,7 +12,6 @@ public sealed partial class ScenarioLoader
private const float MinimumRefineryStock = 0f;
private const float MinimumShipyardStock = 0f;
private const float MinimumSystemSeparation = 3.2f;
private const float StarBubbleRadiusPadding = 40f;
private const float LocalSpaceRadius = 10_000f;
private static readonly string[] GeneratedSystemNames =
[
@@ -51,13 +50,13 @@ public sealed partial class ScenarioLoader
];
private static readonly StarProfile[] StarProfiles =
[
new("main-sequence", "#ffd27a", "#ffb14a", 696340f, 1),
new("blue-white", "#9dc6ff", "#66a0ff", 930000f, 1),
new("white-dwarf", "#f1f5ff", "#b8caff", 12000f, 1),
new("brown-dwarf", "#b97d56", "#8a5438", 70000f, 1),
new("neutron-star", "#d9ebff", "#7ab4ff", 18f, 1),
new("binary-main-sequence", "#ffe09f", "#ffbe6b", 780000f, 2),
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f, 2),
new("main-sequence", "#ffd27a", "#ffb14a", 696340f),
new("blue-white", "#9dc6ff", "#66a0ff", 930000f),
new("white-dwarf", "#f1f5ff", "#b8caff", 12000f),
new("brown-dwarf", "#b97d56", "#8a5438", 70000f),
new("neutron-star", "#d9ebff", "#7ab4ff", 18f),
new("binary-main-sequence", "#ffe09f", "#ffbe6b", 780000f),
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f),
];
private static readonly PlanetProfile[] PlanetProfiles =
[
@@ -88,16 +87,16 @@ public sealed partial class ScenarioLoader
{
var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json");
var systems = ExpandSystems(
InjectSpecialSystems(authoredSystems, _worldGeneration.IncludeSolSystem),
InjectSpecialSystems(authoredSystems),
_worldGeneration.TargetSystemCount);
var scenario = NormalizeScenarioToAvailableSystems(
Read<ScenarioDefinition>("scenario.json"),
systems.Select((system) => system.Id).ToList());
var modules = Read<List<ModuleDefinition>>("modules.json");
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
var ships = Read<List<ShipDefinition>>("ships.json");
var items = Read<List<ItemDefinition>>("items.json");
var items = NormalizeItems(Read<List<ItemDefinition>>("items.json"));
var balance = Read<BalanceDefinition>("balance.json");
var recipes = BuildRecipes(items, ships);
var recipes = BuildRecipes(items, ships, modules);
var moduleRecipes = BuildModuleRecipes(modules);
var moduleDefinitions = modules.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
@@ -177,7 +176,7 @@ public sealed partial class ScenarioLoader
var startingModules = plan.StartingModules.Count > 0
? plan.StartingModules
: ["dock-bay-small", "power-core", "bulk-bay", "liquid-tank"];
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01", "module_arg_stor_liquid_m_01"];
foreach (var moduleId in startingModules)
{
AddStationModule(stations[^1], moduleDefinitions, moduleId);
@@ -187,7 +186,7 @@ public sealed partial class ScenarioLoader
foreach (var station in stations)
{
InitializeStationPopulation(station);
station.Inventory["refined-metals"] = 120f;
station.Inventory["refinedmetals"] = 120f;
if (station.Population > 0f)
{
station.Inventory["water"] = MathF.Max(80f, station.Population * 1.5f);
@@ -195,9 +194,9 @@ public sealed partial class ScenarioLoader
}
var refinery = stations.FirstOrDefault((station) =>
HasInstalledModules(station, "power-core", "liquid-tank") &&
HasInstalledModules(station, "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01") &&
station.SystemId == scenario.MiningDefaults.RefinerySystemId)
?? stations.FirstOrDefault((station) => HasInstalledModules(station, "power-core", "liquid-tank"));
?? stations.FirstOrDefault((station) => HasInstalledModules(station, "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01"));
var patrolRoutes = scenario.PatrolRoutes
.GroupBy((route) => route.SystemId, StringComparer.Ordinal)
@@ -400,12 +399,12 @@ public sealed partial class ScenarioLoader
private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
modules
.Where((module) => module.Construction is not null)
.Where((module) => module.Construction is not null || module.Production.Count > 0)
.Select((module) => new ModuleRecipeDefinition
{
ModuleId = module.Id,
Duration = module.Construction!.ProductionTime,
Inputs = module.Construction.Requirements
Duration = module.Construction?.ProductionTime ?? module.Production[0].Time,
Inputs = (module.Construction?.Requirements ?? module.Production[0].Wares)
.Select((input) => new RecipeInputDefinition
{
ItemId = input.ItemId,
@@ -415,12 +414,54 @@ public sealed partial class ScenarioLoader
})
.ToList();
private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships)
private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships, IReadOnlyCollection<ModuleDefinition> modules)
{
var recipes = new List<RecipeDefinition>();
var preferredProducerByItemId = modules
.Where((module) => module.Products.Count > 0)
.GroupBy((module) => module.Products[0], StringComparer.Ordinal)
.ToDictionary(
(group) => group.Key,
(group) => group.OrderBy((module) => module.Id, StringComparer.Ordinal).First().Id,
StringComparer.Ordinal);
foreach (var item in items)
{
if (item.Production.Count > 0)
{
foreach (var production in item.Production)
{
recipes.Add(new RecipeDefinition
{
Id = $"{item.Id}-{production.Method}-production",
Label = production.Name == "Universal"
? item.Name
: $"{item.Name} ({production.Name})",
FacilityCategory = InferFacilityCategory(item),
Duration = production.Time,
Priority = InferRecipePriority(item),
RequiredModules = InferRequiredModules(item, preferredProducerByItemId),
Inputs = production.Wares
.Select((input) => new RecipeInputDefinition
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
Outputs =
[
new RecipeOutputDefinition
{
ItemId = item.Id,
Amount = production.Amount,
},
],
});
}
continue;
}
if (item.Construction is null)
{
continue;
@@ -481,6 +522,74 @@ public sealed partial class ScenarioLoader
return recipes;
}
private static string InferFacilityCategory(ItemDefinition item) =>
item.Group switch
{
"agricultural" or "food" or "pharmaceutical" or "water" => "farm",
_ => "station",
};
private static List<string> InferRequiredModules(ItemDefinition item, IReadOnlyDictionary<string, string> preferredProducerByItemId)
{
if (preferredProducerByItemId.TryGetValue(item.Id, out var moduleId))
{
return [moduleId];
}
return [];
}
private static int InferRecipePriority(ItemDefinition item) =>
item.Group switch
{
"energy" => 140,
"water" => 130,
"food" => 120,
"agricultural" => 110,
"refined" => 100,
"hightech" => 90,
"shiptech" => 80,
"pharmaceutical" => 70,
_ => 60,
};
private static List<ItemDefinition> NormalizeItems(List<ItemDefinition> items)
{
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.Type))
{
item.Type = string.IsNullOrWhiteSpace(item.Group) ? "material" : item.Group;
}
}
return items;
}
private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
{
foreach (var module in modules)
{
if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product))
{
module.Products = [module.Product];
}
if (string.IsNullOrWhiteSpace(module.ProductionMode))
{
module.ProductionMode = string.Equals(module.Type, "buildmodule", StringComparison.Ordinal)
? "commanded"
: "passive";
}
if (module.WorkforceNeeded <= 0f)
{
module.WorkforceNeeded = module.WorkForce?.Max ?? 0f;
}
}
return modules;
}
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);

View File

@@ -141,9 +141,9 @@ public sealed partial class SimulationEngine
string.Equals(s.Definition.Kind, "construction", StringComparison.Ordinal)),
ControlledSystemCount = GetFactionControlledSystemsCount(world, factionId),
TargetSystemCount = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)),
HasShipFactory = stations.Any(s => s.InstalledModules.Contains("ship-factory", StringComparer.Ordinal)),
HasShipFactory = stations.Any(s => s.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
OreStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "ore")),
RefinedMetalsStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "refined-metals")),
RefinedMetalsStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "refinedmetals")),
};
}

View File

@@ -24,37 +24,18 @@ public sealed partial class SimulationEngine
return local;
}
private static Vector3 ComputeMoonOffset(PlanetDefinition planet, int moonIndex, float timeSeconds)
private static Vector3 ComputeMoonOffset(MoonDefinition moon, float timeSeconds)
{
var orbitRadius = ComputeMoonOrbitRadius(planet, moonIndex);
var speed = ComputeMoonOrbitSpeed(planet, moonIndex);
var phase = HashUnit($"{planet.Label}:{moonIndex}:phase") * MathF.PI * 2f;
var inclination = DegreesToRadians((HashUnit($"{planet.Label}:{moonIndex}:inclination") - 0.5f) * 28f);
var ascendingNode = DegreesToRadians(HashUnit($"{planet.Label}:{moonIndex}:node") * 360f);
var angle = phase + (timeSeconds * speed);
var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch) + (timeSeconds * moon.OrbitSpeed);
var local = new Vector3(
MathF.Cos(angle) * orbitRadius,
MathF.Cos(angle) * moon.OrbitRadius,
0f,
MathF.Sin(angle) * orbitRadius);
local = RotateAroundX(local, inclination);
local = RotateAroundY(local, ascendingNode);
MathF.Sin(angle) * moon.OrbitRadius);
local = RotateAroundX(local, DegreesToRadians(moon.OrbitInclination));
local = RotateAroundY(local, DegreesToRadians(moon.OrbitLongitudeOfAscendingNode));
return local;
}
private static float ComputeMoonOrbitRadius(PlanetDefinition planet, int moonIndex)
{
var spacing = planet.Size * 1.4f;
var variance = HashUnit($"{planet.Label}:{moonIndex}:radius") * planet.Size * 0.9f;
return (planet.Size * 1.8f) + (moonIndex * spacing) + variance;
}
private static float ComputeMoonOrbitSpeed(PlanetDefinition planet, int moonIndex)
{
var radius = ComputeMoonOrbitRadius(planet, moonIndex);
return 0.9f / MathF.Sqrt(MathF.Max(radius, 1f)) + (moonIndex * 0.003f);
}
private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node)
{
var baseSpeed = 0.24f;
@@ -179,10 +160,24 @@ public sealed partial class SimulationEngine
foreach (var system in world.Systems)
{
var starNodeId = $"node-{system.Definition.Id}-star";
if (celestialsById.TryGetValue(starNodeId, out var starNode))
for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
{
starNode.Position = Vector3.Zero;
var star = system.Definition.Stars[starIndex];
var starNodeId = $"node-{system.Definition.Id}-star-{starIndex + 1}";
if (!celestialsById.TryGetValue(starNodeId, out var starNode))
{
continue;
}
if (star.OrbitRadius <= 0f)
{
starNode.Position = Vector3.Zero;
}
else
{
var angle = DegreesToRadians(star.OrbitPhaseAtEpoch) + (worldTimeSeconds * star.OrbitSpeed);
starNode.Position = new Vector3(MathF.Cos(angle) * star.OrbitRadius, 0f, MathF.Sin(angle) * star.OrbitRadius);
}
}
for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
@@ -206,7 +201,7 @@ public sealed partial class SimulationEngine
}
}
for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1)
for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
{
var moonId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
if (!celestialsById.TryGetValue(moonId, out var moonNode))
@@ -214,7 +209,7 @@ public sealed partial class SimulationEngine
continue;
}
moonNode.Position = Add(planetPosition, ComputeMoonOffset(planet, moonIndex, worldTimeSeconds));
moonNode.Position = Add(planetPosition, ComputeMoonOffset(planet.Moons[moonIndex], worldTimeSeconds));
}
}
}

View File

@@ -44,9 +44,9 @@ public sealed partial class SimulationEngine
_ => 0f,
};
var bulkBays = CountStationModules(station, "bulk-bay");
var liquidTanks = CountStationModules(station, "liquid-tank");
var containerBays = CountStationModules(station, "container-bay");
var bulkBays = CountStationModules(station, "module_arg_stor_solid_m_01");
var liquidTanks = CountStationModules(station, "module_arg_stor_liquid_m_01");
var containerBays = CountStationModules(station, "module_arg_stor_container_m_01");
var moduleCapacity = storageClass switch
{
@@ -118,8 +118,8 @@ public sealed partial class SimulationEngine
private static string? GetStorageRequirement(string storageClass) =>
storageClass switch
{
"solid" => "bulk-bay",
"liquid" => "liquid-tank",
"solid" => "module_arg_stor_solid_m_01",
"liquid" => "module_arg_stor_liquid_m_01",
_ => null,
};

View File

@@ -20,15 +20,27 @@ public sealed partial class SimulationEngine
system.Definition.Id,
system.Definition.Label,
ToDto(system.Position),
system.Definition.StarKind,
system.Definition.StarCount,
system.Definition.StarColor,
system.Definition.StarSize,
system.Definition.Stars.Select(star => new StarSnapshot(
star.Kind,
star.Color,
star.Glow,
star.Size,
star.OrbitRadius,
star.OrbitSpeed,
star.OrbitPhaseAtEpoch)).ToList(),
system.Definition.Planets.Select(planet => new PlanetSnapshot(
planet.Label,
planet.PlanetType,
planet.Shape,
planet.MoonCount,
planet.Moons.Select(moon => new MoonSnapshot(
moon.Label,
moon.Size,
moon.Color,
moon.OrbitRadius,
moon.OrbitSpeed,
moon.OrbitPhaseAtEpoch,
moon.OrbitInclination,
moon.OrbitLongitudeOfAscendingNode)).ToList(),
planet.OrbitRadius,
planet.OrbitSpeed,
planet.OrbitEccentricity,

View File

@@ -110,9 +110,9 @@ public sealed partial class SimulationEngine
const float StorageExpansionThreshold = 0.85f;
var storageExpansionCandidates = new[]
{
("solid", "bulk-bay"),
("liquid", "liquid-tank"),
("container", "container-bay"),
("solid", "module_arg_stor_solid_m_01"),
("liquid", "module_arg_stor_liquid_m_01"),
("container", "module_arg_stor_container_m_01"),
};
foreach (var (storageClass, moduleId) in storageExpansionCandidates)
@@ -136,25 +136,25 @@ public sealed partial class SimulationEngine
var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f
? new (string ModuleId, int TargetCount)[]
{
("refinery-stack", 1),
("bulk-bay", 1),
("container-bay", 1),
("fabricator-array", 2),
("component-factory", 1),
("ship-factory", 1),
("dock-bay-small", 2),
("solar-array", 2),
("module_gen_prod_refinedmetals_01", 1),
("module_arg_stor_solid_m_01", 1),
("module_arg_stor_container_m_01", 1),
("module_gen_prod_hullparts_01", 2),
("module_gen_prod_advancedelectronics_01", 1),
("module_gen_build_l_01", 1),
("module_arg_dock_m_01_lowtech", 2),
("module_gen_prod_energycells_01", 2),
}
: new (string ModuleId, int TargetCount)[]
{
("refinery-stack", 1),
("bulk-bay", 1),
("container-bay", 1),
("fabricator-array", 2),
("component-factory", 1),
("ship-factory", 1),
("solar-array", 2),
("dock-bay-small", 2),
("module_gen_prod_refinedmetals_01", 1),
("module_arg_stor_solid_m_01", 1),
("module_arg_stor_container_m_01", 1),
("module_gen_prod_hullparts_01", 2),
("module_gen_prod_advancedelectronics_01", 1),
("module_gen_build_l_01", 1),
("module_gen_prod_energycells_01", 2),
("module_arg_dock_m_01_lowtech", 2),
};
foreach (var (moduleId, targetCount) in priorities)
@@ -225,7 +225,7 @@ public sealed partial class SimulationEngine
}
private static int GetDockingPadCount(StationRuntime station) =>
CountModules(station.InstalledModules, "dock-bay-small") * 2;
CountModules(station.InstalledModules, "module_arg_dock_m_01_lowtech") * 2;
private static int? ReserveDockingPad(StationRuntime station, string shipId)
{

View File

@@ -14,22 +14,22 @@ public sealed partial class SimulationEngine
var desiredOrders = new List<DesiredMarketOrder>();
var waterReserve = MathF.Max(30f, station.Population * 3f);
var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f;
var refinedReserve = HasStationModules(station, "module_gen_prod_hullparts_01") ? 140f : 40f;
var oreReserve = HasRefineryCapability(station) ? 180f : 0f;
var shipPartsReserve = HasStationModules(station, "fabricator-array")
&& !HasStationModules(station, "component-factory", "ship-factory")
var shipPartsReserve = HasStationModules(station, "module_gen_prod_hullparts_01")
&& !HasStationModules(station, "module_gen_prod_advancedelectronics_01", "module_gen_build_l_01")
&& FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships")
? 90f
: 0f;
AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f);
AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f);
AddDemandOrder(desiredOrders, station, "refined-metals", refinedReserve, valuationBase: 1.15f);
AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f);
AddDemandOrder(desiredOrders, station, "refinedmetals", refinedReserve, valuationBase: 1.15f);
AddDemandOrder(desiredOrders, station, "hullparts", shipPartsReserve, valuationBase: 1.3f);
AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f);
AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f);
AddSupplyOrder(desiredOrders, station, "refined-metals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
AddSupplyOrder(desiredOrders, station, "refinedmetals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
ReconcileStationMarketOrders(world, station, desiredOrders);
}
@@ -133,7 +133,7 @@ public sealed partial class SimulationEngine
var fleetPressure = FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships") ? 1f : 0f;
priority += recipe.Id switch
{
"ship-parts-integration" => HasStationModules(station, "component-factory", "ship-factory")
"ship-parts-integration" => HasStationModules(station, "module_gen_prod_advancedelectronics_01", "module_gen_build_l_01")
? -140f * MathF.Max(expansionPressure, fleetPressure)
: 280f * MathF.Max(expansionPressure, fleetPressure),
"hull-fabrication" => 180f * expansionPressure,
@@ -211,7 +211,7 @@ public sealed partial class SimulationEngine
}
private static bool HasRefineryCapability(StationRuntime station) =>
HasStationModules(station, "refinery-stack", "power-core", "bulk-bay");
HasStationModules(station, "module_gen_prod_refinedmetals_01", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01");
private static void AddDemandOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase)
{

View File

@@ -31,7 +31,7 @@ public sealed partial class SimulationEngine
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01");
station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied)

View File

@@ -4,5 +4,4 @@ public sealed class WorldGenerationOptions
{
public int TargetSystemCount { get; init; } = 160;
public bool IncludeSolSystem { get; init; } = true;
}