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; 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( public sealed record SystemSnapshot(
string Id, string Id,
string Label, string Label,
Vector3Dto GalaxyPosition, Vector3Dto GalaxyPosition,
string StarKind, IReadOnlyList<StarSnapshot> Stars,
int StarCount,
string StarColor,
float StarSize,
IReadOnlyList<PlanetSnapshot> Planets); IReadOnlyList<PlanetSnapshot> Planets);
public sealed record PlanetSnapshot( public sealed record PlanetSnapshot(
string Label, string Label,
string PlanetType, string PlanetType,
string Shape, string Shape,
int MoonCount, IReadOnlyList<MoonSnapshot> Moons,
float OrbitRadius, float OrbitRadius,
float OrbitSpeed, float OrbitSpeed,
float OrbitEccentricity, float OrbitEccentricity,

View File

@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Data;
public sealed class ConstructionDefinition public sealed class ConstructionDefinition
@@ -13,6 +15,29 @@ public sealed class ConstructionDefinition
public int Priority { get; set; } 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 sealed class BalanceDefinition
{ {
public float YPlane { get; set; } public float YPlane { get; set; }
@@ -25,17 +50,35 @@ public sealed class BalanceDefinition
public float UndockDistance { get; set; } 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 sealed class SolarSystemDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Label { get; set; } public required string Label { get; set; }
public required float[] Position { get; set; } public required float[] Position { get; set; }
public string StarKind { get; set; } = "main-sequence"; public required List<StarDefinition> Stars { get; set; }
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 AsteroidFieldDefinition AsteroidField { get; set; } public required AsteroidFieldDefinition AsteroidField { get; set; }
public required List<ResourceNodeDefinition> ResourceNodes { get; set; } public required List<ResourceNodeDefinition> ResourceNodes { get; set; }
public required List<PlanetDefinition> Planets { get; set; } public required List<PlanetDefinition> Planets { get; set; }
@@ -68,9 +111,21 @@ public sealed class ItemDefinition
public required string Name { get; set; } public required string Name { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public string Type { get; set; } = "material"; 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 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; } public ConstructionDefinition? Construction { get; set; }
[JsonPropertyName("transport")]
public string Transport
{
set => CargoKind = value;
}
} }
public sealed class RecipeOutputDefinition public sealed class RecipeOutputDefinition
@@ -81,8 +136,13 @@ public sealed class RecipeOutputDefinition
public sealed class RecipeInputDefinition public sealed class RecipeInputDefinition
{ {
public required string ItemId { get; set; } public string ItemId { get; set; } = string.Empty;
public float Amount { get; set; } public float Amount { get; set; }
[JsonPropertyName("ware")]
public string Ware
{
set => ItemId = value;
}
} }
public sealed class ModuleConstructionDefinition public sealed class ModuleConstructionDefinition
@@ -91,18 +151,73 @@ public sealed class ModuleConstructionDefinition
public float ProductionTime { get; set; } 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 sealed class ModuleDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public required string Type { get; set; } public required string Type { get; set; }
[JsonIgnore]
public string? Product { get; set; } public string? Product { get; set; }
public List<string> Products { get; set; } = [];
public string ProductionMode { get; set; } = "passive"; public string ProductionMode { get; set; } = "passive";
public float Radius { get; set; } = 12f; public float Radius { get; set; } = 12f;
public float Hull { get; set; } = 100f; public float Hull { get; set; } = 100f;
public float WorkforceNeeded { get; set; } 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; } public ModuleConstructionDefinition? Construction { get; set; }
[JsonPropertyName("product")]
public List<string> ProductIds
{
set => Products = value ?? [];
}
} }
public sealed class ModuleRecipeDefinition public sealed class ModuleRecipeDefinition
@@ -130,7 +245,7 @@ public sealed class PlanetDefinition
public required string Label { get; set; } public required string Label { get; set; }
public string PlanetType { get; set; } = "terrestrial"; public string PlanetType { get; set; } = "terrestrial";
public string Shape { get; set; } = "sphere"; public string Shape { get; set; } = "sphere";
public int MoonCount { get; set; } public List<MoonDefinition> Moons { get; set; } = [];
public float OrbitRadius { get; set; } public float OrbitRadius { get; set; }
public float OrbitSpeed { get; set; } public float OrbitSpeed { get; set; }
public float OrbitEccentricity { get; set; } public float OrbitEccentricity { get; set; }

View File

@@ -8,19 +8,11 @@ public sealed partial class ScenarioLoader
private const string DevelopmentCompanionSystemId = "helios"; private const string DevelopmentCompanionSystemId = "helios";
private static List<SolarSystemDefinition> InjectSpecialSystems( private static List<SolarSystemDefinition> InjectSpecialSystems(
IReadOnlyList<SolarSystemDefinition> authoredSystems, IReadOnlyList<SolarSystemDefinition> authoredSystems)
bool includeSolSystem)
{ {
var systems = authoredSystems return authoredSystems
.Select(CloneSystemDefinition) .Select(CloneSystemDefinition)
.ToList(); .ToList();
if (includeSolSystem && systems.All((system) => system.Id != "sol"))
{
systems.Add(CreateSolSystem());
}
return systems;
} }
private static List<SolarSystemDefinition> ExpandSystems( private static List<SolarSystemDefinition> ExpandSystems(
@@ -156,12 +148,16 @@ public sealed partial class ScenarioLoader
Id = id, Id = id,
Label = label, Label = label,
Position = [position.X, position.Y, position.Z], Position = [position.X, position.Y, position.Z],
StarKind = starProfile.Kind, Stars =
StarCount = starProfile.StarCount, [
StarColor = starProfile.StarColor, new StarDefinition
StarGlow = starProfile.StarGlow, {
StarSize = starProfile.BaseSize + ((generatedIndex % 4) * 2f), Kind = starProfile.Kind,
GravityWellRadius = template.GravityWellRadius + ((generatedIndex % 3) * 12f), Color = starProfile.StarColor,
Glow = starProfile.StarGlow,
Size = starProfile.BaseSize + ((generatedIndex % 4) * 2f),
},
],
AsteroidField = new AsteroidFieldDefinition AsteroidField = new AsteroidFieldDefinition
{ {
DecorationCount = template.AsteroidField.DecorationCount + ((generatedIndex % 5) * 10), DecorationCount = template.AsteroidField.DecorationCount + ((generatedIndex % 5) * 10),
@@ -181,12 +177,7 @@ public sealed partial class ScenarioLoader
Id = definition.Id, Id = definition.Id,
Label = definition.Label, Label = definition.Label,
Position = definition.Position.ToArray(), Position = definition.Position.ToArray(),
StarKind = definition.StarKind, 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(),
StarCount = definition.StarCount,
StarColor = definition.StarColor,
StarGlow = definition.StarGlow,
StarSize = definition.StarSize,
GravityWellRadius = definition.GravityWellRadius,
AsteroidField = new AsteroidFieldDefinition AsteroidField = new AsteroidFieldDefinition
{ {
DecorationCount = definition.AsteroidField.DecorationCount, DecorationCount = definition.AsteroidField.DecorationCount,
@@ -214,7 +205,7 @@ public sealed partial class ScenarioLoader
Label = planet.Label, Label = planet.Label,
PlanetType = planet.PlanetType, PlanetType = planet.PlanetType,
Shape = planet.Shape, 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, OrbitRadius = planet.OrbitRadius,
OrbitSpeed = planet.OrbitSpeed, OrbitSpeed = planet.OrbitSpeed,
OrbitEccentricity = planet.OrbitEccentricity, OrbitEccentricity = planet.OrbitEccentricity,
@@ -387,14 +378,15 @@ public sealed partial class ScenarioLoader
orbitRadius += profile.OrbitGapMin + (Hash01(generatedIndex, 10 + index) * (profile.OrbitGapMax - profile.OrbitGapMin)); orbitRadius += profile.OrbitGapMin + (Hash01(generatedIndex, 10 + index) * (profile.OrbitGapMax - profile.OrbitGapMin));
var orbitEccentricity = 0.01f + (Hash01(generatedIndex, 20 + index) * 0.16f); var orbitEccentricity = 0.01f + (Hash01(generatedIndex, 20 + index) * 0.16f);
var orbitInclination = -9f + (Hash01(generatedIndex, 30 + index) * 18f); 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 planets.Add(new PlanetDefinition
{ {
Label = $"{BuildPlanetBaseName(generatedIndex, index)}-{index + 1}", Label = planetLabel,
PlanetType = profile.Type, PlanetType = profile.Type,
Shape = profile.Shape, Shape = profile.Shape,
MoonCount = profile.BaseMoonCount + moonVariance, Moons = GenerateMoons(planetLabel, profile.BaseSize, moonCount),
OrbitRadius = orbitRadius, OrbitRadius = orbitRadius,
OrbitSpeed = 0.11f / MathF.Sqrt(MathF.Max(orbitRadius * orbitRadius * orbitRadius, 0.02f)), OrbitSpeed = 0.11f / MathF.Sqrt(MathF.Max(orbitRadius * orbitRadius * orbitRadius, 0.02f)),
OrbitEccentricity = orbitEccentricity, OrbitEccentricity = orbitEccentricity,
@@ -471,12 +463,44 @@ public sealed partial class ScenarioLoader
return (value & 0x00ffffff) / 16777215f; 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( private sealed record StarProfile(
string Kind, string Kind,
string StarColor, string StarColor,
string StarGlow, string StarGlow,
float BaseSize, float BaseSize);
int StarCount);
private sealed record PlanetProfile( private sealed record PlanetProfile(
string Type, string Type,
@@ -490,106 +514,4 @@ public sealed partial class ScenarioLoader
public float OrbitGapMax => OrbitGapMin + MathF.Max(0.12f, OrbitGapMin * 0.45f); 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(); .ToList();
var refineries = ownedStations 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(); .ToList();
if (refineries.Count > 0) if (refineries.Count > 0)
{ {
foreach (var refinery in refineries) 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)) 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)[] foreach (var (moduleId, targetCount) in new (string ModuleId, int TargetCount)[]
{ {
("refinery-stack", 1), ("module_gen_prod_refinedmetals_01", 1),
("container-bay", 1), ("module_arg_stor_container_m_01", 1),
("fabricator-array", 2), ("module_gen_prod_hullparts_01", 2),
("component-factory", 1), ("module_gen_prod_advancedelectronics_01", 1),
("ship-factory", 1), ("module_gen_build_l_01", 1),
("solar-array", 2), ("module_gen_prod_energycells_01", 2),
("dock-bay-small", 2), ("module_arg_dock_m_01_lowtech", 2),
}) })
{ {
if (CountModules(station.InstalledModules, moduleId) < targetCount if (CountModules(station.InstalledModules, moduleId) < targetCount
@@ -210,7 +210,7 @@ public sealed partial class ScenarioLoader
private static void InitializeStationPopulation(StationRuntime station) 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.PopulationCapacity = 40f + (habitatModules * 220f);
station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
station.Population = habitatModules > 0 station.Population = habitatModules > 0

View File

@@ -9,13 +9,18 @@ public sealed partial class ScenarioLoader
var celestials = new List<CelestialRuntime>(); var celestials = new List<CelestialRuntime>();
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>(); var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();
AddCelestial( for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
celestials, {
id: $"node-{system.Definition.Id}-star", AddCelestial(
systemId: system.Definition.Id, celestials,
kind: SpatialNodeKind.Star, id: $"node-{system.Definition.Id}-star-{starIndex + 1}",
position: Vector3.Zero, systemId: system.Definition.Id,
localSpaceRadius: MathF.Max(system.Definition.GravityWellRadius + StarBubbleRadiusPadding, LocalSpaceRadius)); 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) for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
{ {
@@ -29,7 +34,7 @@ public sealed partial class ScenarioLoader
kind: SpatialNodeKind.Planet, kind: SpatialNodeKind.Planet,
position: planetPosition, position: planetPosition,
localSpaceRadius: LocalSpaceRadius, localSpaceRadius: LocalSpaceRadius,
parentNodeId: $"node-{system.Definition.Id}-star"); parentNodeId: primaryStarNodeId);
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal); var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet)) foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
@@ -48,15 +53,10 @@ public sealed partial class ScenarioLoader
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes; lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
if (planet.MoonCount <= 0) for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
{ {
continue; var moon = planet.Moons[moonIndex];
} var moonPosition = ComputeMoonPosition(planetPosition, moon);
var moonOrbitRadius = MathF.Max(planet.Size + 48f, 42f);
for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1)
{
var moonPosition = ComputeMoonPosition(planetPosition, moonOrbitRadius, moonIndex, planetIndex);
AddCelestial( AddCelestial(
celestials, celestials,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}", id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
@@ -65,7 +65,6 @@ public sealed partial class ScenarioLoader
position: moonPosition, position: moonPosition,
localSpaceRadius: LocalSpaceRadius, localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id); parentNodeId: planetCelestial.Id);
moonOrbitRadius += 30f;
} }
} }
@@ -232,10 +231,11 @@ public sealed partial class ScenarioLoader
return new Vector3(x, 0f, z); 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); var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch);
return Add(planetPosition, new Vector3(MathF.Cos(angle) * orbitRadius, 0f, MathF.Sin(angle) * orbitRadius)); 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) 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 MinimumRefineryStock = 0f;
private const float MinimumShipyardStock = 0f; private const float MinimumShipyardStock = 0f;
private const float MinimumSystemSeparation = 3.2f; private const float MinimumSystemSeparation = 3.2f;
private const float StarBubbleRadiusPadding = 40f;
private const float LocalSpaceRadius = 10_000f; private const float LocalSpaceRadius = 10_000f;
private static readonly string[] GeneratedSystemNames = private static readonly string[] GeneratedSystemNames =
[ [
@@ -51,13 +50,13 @@ public sealed partial class ScenarioLoader
]; ];
private static readonly StarProfile[] StarProfiles = private static readonly StarProfile[] StarProfiles =
[ [
new("main-sequence", "#ffd27a", "#ffb14a", 696340f, 1), new("main-sequence", "#ffd27a", "#ffb14a", 696340f),
new("blue-white", "#9dc6ff", "#66a0ff", 930000f, 1), new("blue-white", "#9dc6ff", "#66a0ff", 930000f),
new("white-dwarf", "#f1f5ff", "#b8caff", 12000f, 1), new("white-dwarf", "#f1f5ff", "#b8caff", 12000f),
new("brown-dwarf", "#b97d56", "#8a5438", 70000f, 1), new("brown-dwarf", "#b97d56", "#8a5438", 70000f),
new("neutron-star", "#d9ebff", "#7ab4ff", 18f, 1), new("neutron-star", "#d9ebff", "#7ab4ff", 18f),
new("binary-main-sequence", "#ffe09f", "#ffbe6b", 780000f, 2), new("binary-main-sequence", "#ffe09f", "#ffbe6b", 780000f),
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f, 2), new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f),
]; ];
private static readonly PlanetProfile[] PlanetProfiles = private static readonly PlanetProfile[] PlanetProfiles =
[ [
@@ -88,16 +87,16 @@ public sealed partial class ScenarioLoader
{ {
var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json"); var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json");
var systems = ExpandSystems( var systems = ExpandSystems(
InjectSpecialSystems(authoredSystems, _worldGeneration.IncludeSolSystem), InjectSpecialSystems(authoredSystems),
_worldGeneration.TargetSystemCount); _worldGeneration.TargetSystemCount);
var scenario = NormalizeScenarioToAvailableSystems( var scenario = NormalizeScenarioToAvailableSystems(
Read<ScenarioDefinition>("scenario.json"), Read<ScenarioDefinition>("scenario.json"),
systems.Select((system) => system.Id).ToList()); 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 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 balance = Read<BalanceDefinition>("balance.json");
var recipes = BuildRecipes(items, ships); var recipes = BuildRecipes(items, ships, modules);
var moduleRecipes = BuildModuleRecipes(modules); var moduleRecipes = BuildModuleRecipes(modules);
var moduleDefinitions = modules.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var moduleDefinitions = modules.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
@@ -177,7 +176,7 @@ public sealed partial class ScenarioLoader
var startingModules = plan.StartingModules.Count > 0 var startingModules = plan.StartingModules.Count > 0
? plan.StartingModules ? 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) foreach (var moduleId in startingModules)
{ {
AddStationModule(stations[^1], moduleDefinitions, moduleId); AddStationModule(stations[^1], moduleDefinitions, moduleId);
@@ -187,7 +186,7 @@ public sealed partial class ScenarioLoader
foreach (var station in stations) foreach (var station in stations)
{ {
InitializeStationPopulation(station); InitializeStationPopulation(station);
station.Inventory["refined-metals"] = 120f; station.Inventory["refinedmetals"] = 120f;
if (station.Population > 0f) if (station.Population > 0f)
{ {
station.Inventory["water"] = MathF.Max(80f, station.Population * 1.5f); station.Inventory["water"] = MathF.Max(80f, station.Population * 1.5f);
@@ -195,9 +194,9 @@ public sealed partial class ScenarioLoader
} }
var refinery = stations.FirstOrDefault((station) => 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) 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 var patrolRoutes = scenario.PatrolRoutes
.GroupBy((route) => route.SystemId, StringComparer.Ordinal) .GroupBy((route) => route.SystemId, StringComparer.Ordinal)
@@ -400,12 +399,12 @@ public sealed partial class ScenarioLoader
private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) => private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
modules modules
.Where((module) => module.Construction is not null) .Where((module) => module.Construction is not null || module.Production.Count > 0)
.Select((module) => new ModuleRecipeDefinition .Select((module) => new ModuleRecipeDefinition
{ {
ModuleId = module.Id, ModuleId = module.Id,
Duration = module.Construction!.ProductionTime, Duration = module.Construction?.ProductionTime ?? module.Production[0].Time,
Inputs = module.Construction.Requirements Inputs = (module.Construction?.Requirements ?? module.Production[0].Wares)
.Select((input) => new RecipeInputDefinition .Select((input) => new RecipeInputDefinition
{ {
ItemId = input.ItemId, ItemId = input.ItemId,
@@ -415,12 +414,54 @@ public sealed partial class ScenarioLoader
}) })
.ToList(); .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 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) 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) if (item.Construction is null)
{ {
continue; continue;
@@ -481,6 +522,74 @@ public sealed partial class ScenarioLoader
return recipes; 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 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); 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)), string.Equals(s.Definition.Kind, "construction", StringComparison.Ordinal)),
ControlledSystemCount = GetFactionControlledSystemsCount(world, factionId), ControlledSystemCount = GetFactionControlledSystemsCount(world, factionId),
TargetSystemCount = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)), 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")), 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; 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 angle = DegreesToRadians(moon.OrbitPhaseAtEpoch) + (timeSeconds * moon.OrbitSpeed);
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 local = new Vector3( var local = new Vector3(
MathF.Cos(angle) * orbitRadius, MathF.Cos(angle) * moon.OrbitRadius,
0f, 0f,
MathF.Sin(angle) * orbitRadius); MathF.Sin(angle) * moon.OrbitRadius);
local = RotateAroundX(local, inclination); local = RotateAroundX(local, DegreesToRadians(moon.OrbitInclination));
local = RotateAroundY(local, ascendingNode); local = RotateAroundY(local, DegreesToRadians(moon.OrbitLongitudeOfAscendingNode));
return local; 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) private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node)
{ {
var baseSpeed = 0.24f; var baseSpeed = 0.24f;
@@ -179,10 +160,24 @@ public sealed partial class SimulationEngine
foreach (var system in world.Systems) foreach (var system in world.Systems)
{ {
var starNodeId = $"node-{system.Definition.Id}-star"; for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
if (celestialsById.TryGetValue(starNodeId, out var starNode))
{ {
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) 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}"; var moonId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
if (!celestialsById.TryGetValue(moonId, out var moonNode)) if (!celestialsById.TryGetValue(moonId, out var moonNode))
@@ -214,7 +209,7 @@ public sealed partial class SimulationEngine
continue; 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, _ => 0f,
}; };
var bulkBays = CountStationModules(station, "bulk-bay"); var bulkBays = CountStationModules(station, "module_arg_stor_solid_m_01");
var liquidTanks = CountStationModules(station, "liquid-tank"); var liquidTanks = CountStationModules(station, "module_arg_stor_liquid_m_01");
var containerBays = CountStationModules(station, "container-bay"); var containerBays = CountStationModules(station, "module_arg_stor_container_m_01");
var moduleCapacity = storageClass switch var moduleCapacity = storageClass switch
{ {
@@ -118,8 +118,8 @@ public sealed partial class SimulationEngine
private static string? GetStorageRequirement(string storageClass) => private static string? GetStorageRequirement(string storageClass) =>
storageClass switch storageClass switch
{ {
"solid" => "bulk-bay", "solid" => "module_arg_stor_solid_m_01",
"liquid" => "liquid-tank", "liquid" => "module_arg_stor_liquid_m_01",
_ => null, _ => null,
}; };

View File

@@ -20,15 +20,27 @@ public sealed partial class SimulationEngine
system.Definition.Id, system.Definition.Id,
system.Definition.Label, system.Definition.Label,
ToDto(system.Position), ToDto(system.Position),
system.Definition.StarKind, system.Definition.Stars.Select(star => new StarSnapshot(
system.Definition.StarCount, star.Kind,
system.Definition.StarColor, star.Color,
system.Definition.StarSize, star.Glow,
star.Size,
star.OrbitRadius,
star.OrbitSpeed,
star.OrbitPhaseAtEpoch)).ToList(),
system.Definition.Planets.Select(planet => new PlanetSnapshot( system.Definition.Planets.Select(planet => new PlanetSnapshot(
planet.Label, planet.Label,
planet.PlanetType, planet.PlanetType,
planet.Shape, 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.OrbitRadius,
planet.OrbitSpeed, planet.OrbitSpeed,
planet.OrbitEccentricity, planet.OrbitEccentricity,

View File

@@ -110,9 +110,9 @@ public sealed partial class SimulationEngine
const float StorageExpansionThreshold = 0.85f; const float StorageExpansionThreshold = 0.85f;
var storageExpansionCandidates = new[] var storageExpansionCandidates = new[]
{ {
("solid", "bulk-bay"), ("solid", "module_arg_stor_solid_m_01"),
("liquid", "liquid-tank"), ("liquid", "module_arg_stor_liquid_m_01"),
("container", "container-bay"), ("container", "module_arg_stor_container_m_01"),
}; };
foreach (var (storageClass, moduleId) in storageExpansionCandidates) foreach (var (storageClass, moduleId) in storageExpansionCandidates)
@@ -136,25 +136,25 @@ public sealed partial class SimulationEngine
var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f
? new (string ModuleId, int TargetCount)[] ? new (string ModuleId, int TargetCount)[]
{ {
("refinery-stack", 1), ("module_gen_prod_refinedmetals_01", 1),
("bulk-bay", 1), ("module_arg_stor_solid_m_01", 1),
("container-bay", 1), ("module_arg_stor_container_m_01", 1),
("fabricator-array", 2), ("module_gen_prod_hullparts_01", 2),
("component-factory", 1), ("module_gen_prod_advancedelectronics_01", 1),
("ship-factory", 1), ("module_gen_build_l_01", 1),
("dock-bay-small", 2), ("module_arg_dock_m_01_lowtech", 2),
("solar-array", 2), ("module_gen_prod_energycells_01", 2),
} }
: new (string ModuleId, int TargetCount)[] : new (string ModuleId, int TargetCount)[]
{ {
("refinery-stack", 1), ("module_gen_prod_refinedmetals_01", 1),
("bulk-bay", 1), ("module_arg_stor_solid_m_01", 1),
("container-bay", 1), ("module_arg_stor_container_m_01", 1),
("fabricator-array", 2), ("module_gen_prod_hullparts_01", 2),
("component-factory", 1), ("module_gen_prod_advancedelectronics_01", 1),
("ship-factory", 1), ("module_gen_build_l_01", 1),
("solar-array", 2), ("module_gen_prod_energycells_01", 2),
("dock-bay-small", 2), ("module_arg_dock_m_01_lowtech", 2),
}; };
foreach (var (moduleId, targetCount) in priorities) foreach (var (moduleId, targetCount) in priorities)
@@ -225,7 +225,7 @@ public sealed partial class SimulationEngine
} }
private static int GetDockingPadCount(StationRuntime station) => 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) 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 desiredOrders = new List<DesiredMarketOrder>();
var waterReserve = MathF.Max(30f, station.Population * 3f); 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 oreReserve = HasRefineryCapability(station) ? 180f : 0f;
var shipPartsReserve = HasStationModules(station, "fabricator-array") var shipPartsReserve = HasStationModules(station, "module_gen_prod_hullparts_01")
&& !HasStationModules(station, "component-factory", "ship-factory") && !HasStationModules(station, "module_gen_prod_advancedelectronics_01", "module_gen_build_l_01")
&& FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships") && FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships")
? 90f ? 90f
: 0f; : 0f;
AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f); AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f);
AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f); AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f);
AddDemandOrder(desiredOrders, station, "refined-metals", refinedReserve, valuationBase: 1.15f); AddDemandOrder(desiredOrders, station, "refinedmetals", refinedReserve, valuationBase: 1.15f);
AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f); AddDemandOrder(desiredOrders, station, "hullparts", shipPartsReserve, valuationBase: 1.3f);
AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f); 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, "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); ReconcileStationMarketOrders(world, station, desiredOrders);
} }
@@ -133,7 +133,7 @@ public sealed partial class SimulationEngine
var fleetPressure = FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships") ? 1f : 0f; var fleetPressure = FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships") ? 1f : 0f;
priority += recipe.Id switch 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) ? -140f * MathF.Max(expansionPressure, fleetPressure)
: 280f * MathF.Max(expansionPressure, fleetPressure), : 280f * MathF.Max(expansionPressure, fleetPressure),
"hull-fabrication" => 180f * expansionPressure, "hull-fabrication" => 180f * expansionPressure,
@@ -211,7 +211,7 @@ public sealed partial class SimulationEngine
} }
private static bool HasRefineryCapability(StationRuntime station) => 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) 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 requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater); var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= 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); station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied) if (waterSatisfied)

View File

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

View File

@@ -28,19 +28,12 @@ import { SystemLayer } from "./viewerSystemLayer";
import { LocalLayer } from "./viewerLocalLayer"; import { LocalLayer } from "./viewerLocalLayer";
import type { FactionSnapshot } from "./contracts"; import type { FactionSnapshot } from "./contracts";
import type { import type {
CelestialVisual,
CameraMode, CameraMode,
ClaimVisual,
ConstructionSiteVisual,
DragMode, DragMode,
HistoryWindowState, HistoryWindowState,
NetworkStats, NetworkStats,
NodeVisual,
OrbitLineVisual,
PerformanceStats, PerformanceStats,
Selectable, Selectable,
ShipVisual,
StructureVisual,
SystemVisual, SystemVisual,
WorldState, WorldState,
PovLevel, PovLevel,
@@ -51,10 +44,10 @@ export class ViewerAppController {
private readonly renderer = new THREE.WebGLRenderer({ antialias: true }); private readonly renderer = new THREE.WebGLRenderer({ antialias: true });
// ── Three independent rendering layers ─────────────────────────────────── // ── Three independent rendering layers ───────────────────────────────────
private readonly universeLayer = new UniverseLayer(); readonly universeLayer = new UniverseLayer();
private readonly galaxyLayer = new GalaxyLayer(); readonly galaxyLayer = new GalaxyLayer();
private readonly systemLayer = new SystemLayer(); readonly systemLayer = new SystemLayer();
private readonly localLayer = new LocalLayer(); readonly localLayer = new LocalLayer();
private readonly clock = new THREE.Clock(); private readonly clock = new THREE.Clock();
private readonly raycaster = new THREE.Raycaster(); private readonly raycaster = new THREE.Raycaster();
@@ -70,16 +63,6 @@ export class ViewerAppController {
private readonly gamePanelEl: HTMLDivElement; private readonly gamePanelEl: HTMLDivElement;
private readonly celestialVisuals = new Map<string, CelestialVisual>();
private readonly stationVisuals = new Map<string, StructureVisual>();
private readonly claimVisuals = new Map<string, ClaimVisual>();
private readonly constructionSiteVisuals = new Map<string, ConstructionSiteVisual>();
private readonly shipVisuals = new Map<string, ShipVisual>();
private readonly systemVisuals = new Map<string, SystemVisual>();
private readonly nodeVisuals = new Map<string, NodeVisual>();
private readonly planetVisuals: any[] = [];
private readonly orbitLines: OrbitLineVisual[] = [];
private readonly statusEl: HTMLDivElement; private readonly statusEl: HTMLDivElement;
private readonly gameSummaryEl: HTMLSpanElement; private readonly gameSummaryEl: HTMLSpanElement;
private readonly systemPanelEl: HTMLDivElement; private readonly systemPanelEl: HTMLDivElement;
@@ -98,6 +81,7 @@ export class ViewerAppController {
private readonly historyLayerEl: HTMLDivElement; private readonly historyLayerEl: HTMLDivElement;
private readonly marqueeEl: HTMLDivElement; private readonly marqueeEl: HTMLDivElement;
private readonly hoverLabelEl: HTMLDivElement; private readonly hoverLabelEl: HTMLDivElement;
private readonly hoverConnectorLineEl: SVGLineElement;
private world?: WorldState; private world?: WorldState;
private worldTimeSyncMs = performance.now(); private worldTimeSyncMs = performance.now();
@@ -165,6 +149,7 @@ export class ViewerAppController {
this.historyLayerEl = hud.historyLayerEl; this.historyLayerEl = hud.historyLayerEl;
this.marqueeEl = hud.marqueeEl; this.marqueeEl = hud.marqueeEl;
this.hoverLabelEl = hud.hoverLabelEl; this.hoverLabelEl = hud.hoverLabelEl;
this.hoverConnectorLineEl = hud.hoverConnectorLineEl;
({ ({
sceneDataController: this.sceneDataController, sceneDataController: this.sceneDataController,
navigationController: this.navigationController, navigationController: this.navigationController,
@@ -231,13 +216,10 @@ export class ViewerAppController {
renderFrame({ renderFrame({
clock: this.clock, clock: this.clock,
renderer: this.renderer, renderer: this.renderer,
universeScene: this.universeLayer.scene, universeLayer: this.universeLayer,
galaxyScene: this.galaxyLayer.scene, galaxyLayer: this.galaxyLayer,
galaxyCamera: this.galaxyLayer.camera, systemLayer: this.systemLayer,
systemScene: this.systemLayer.scene, localLayer: this.localLayer,
systemCamera: this.systemLayer.camera,
localScene: this.localLayer.scene,
localCamera: this.localLayer.camera,
getPovLevel: () => this.povLevel, getPovLevel: () => this.povLevel,
updateCamera: (delta) => this.updateCamera(delta), updateCamera: (delta) => this.updateCamera(delta),
updateAmbience: (delta) => this.presentationController.updateAmbience(delta), updateAmbience: (delta) => this.presentationController.updateAmbience(delta),
@@ -294,7 +276,7 @@ export class ViewerAppController {
// Update star dot scales in galaxy scene // Update star dot scales in galaxy scene
updateSystemStarPresentation( updateSystemStarPresentation(
this.systemVisuals, this.galaxyLayer.systemVisuals,
this.activeSystemId, this.activeSystemId,
this.galaxyLayer.camera, this.galaxyLayer.camera,
(sprite, opacity) => this.setShellReticleOpacity(sprite, opacity), (sprite, opacity) => this.setShellReticleOpacity(sprite, opacity),
@@ -343,9 +325,9 @@ export class ViewerAppController {
private onResize = () => { private onResize = () => {
resizeViewer({ resizeViewer({
renderer: this.renderer, renderer: this.renderer,
galaxyCamera: this.galaxyLayer.camera, galaxyLayer: this.galaxyLayer,
systemCamera: this.systemLayer.camera, systemLayer: this.systemLayer,
localCamera: this.localLayer.camera, localLayer: this.localLayer,
}); });
}; };
@@ -354,7 +336,7 @@ export class ViewerAppController {
} }
private describeSelectionParent(selection: Selectable) { private describeSelectionParent(selection: Selectable) {
return describeSelectionParent(this.world, selection, this.stationVisuals, this.nodeVisuals); return describeSelectionParent(this.world, selection, this.systemLayer.stationVisuals, this.systemLayer.nodeVisuals);
} }
private toDisplayLocalPosition(localPosition: THREE.Vector3, systemId?: string) { private toDisplayLocalPosition(localPosition: THREE.Vector3, systemId?: string) {

View File

@@ -7,6 +7,8 @@ export type {
OrbitalSimulationSnapshot, OrbitalSimulationSnapshot,
} from "./contractsWorld"; } from "./contractsWorld";
export type { export type {
StarSnapshot,
MoonSnapshot,
SystemSnapshot, SystemSnapshot,
PlanetSnapshot, PlanetSnapshot,
ResourceNodeSnapshot, ResourceNodeSnapshot,

View File

@@ -1,13 +1,31 @@
import type { Vector3Dto } from "./contractsCommon"; import type { Vector3Dto } from "./contractsCommon";
export interface StarSnapshot {
kind: string;
color: string;
glow: string;
size: number;
orbitRadius: number;
orbitSpeed: number;
orbitPhaseAtEpoch: number;
}
export interface MoonSnapshot {
label: string;
size: number;
color: string;
orbitRadius: number;
orbitSpeed: number;
orbitPhaseAtEpoch: number;
orbitInclination: number;
orbitLongitudeOfAscendingNode: number;
}
export interface SystemSnapshot { export interface SystemSnapshot {
id: string; id: string;
label: string; label: string;
galaxyPosition: Vector3Dto; galaxyPosition: Vector3Dto;
starKind: string; stars: StarSnapshot[];
starCount: number;
starColor: string;
starSize: number;
planets: PlanetSnapshot[]; planets: PlanetSnapshot[];
} }
@@ -15,7 +33,7 @@ export interface PlanetSnapshot {
label: string; label: string;
planetType: string; planetType: string;
shape: string; shape: string;
moonCount: number; moons: MoonSnapshot[];
orbitRadius: number; orbitRadius: number;
orbitSpeed: number; orbitSpeed: number;
orbitEccentricity: number; orbitEccentricity: number;

View File

@@ -65,6 +65,25 @@ canvas {
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
} }
.hover-connector-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: visible;
}
.hover-connector-line {
stroke: rgba(255, 88, 72, 0.45);
stroke-width: 1.5;
stroke-dasharray: 4 3;
}
.hover-connector-line[hidden] {
display: none;
}
.hover-label { .hover-label {
position: absolute; position: absolute;
padding: 8px 10px; padding: 8px 10px;

View File

@@ -215,6 +215,12 @@ export function resolveSelectionPosition(params: ResolveSelectionPositionParams)
return computePlanetLocalPosition(planet, currentWorldTimeSeconds(world, worldTimeSyncMs)); return computePlanetLocalPosition(planet, currentWorldTimeSeconds(world, worldTimeSyncMs));
} }
if (selection.kind === "moon") {
const system = world.systems.get(selection.systemId);
const planet = system?.planets[selection.planetIndex];
return planet ? computePlanetLocalPosition(planet, currentWorldTimeSeconds(world, worldTimeSyncMs)) : undefined;
}
const system = world.systems.get(selection.id); const system = world.systems.get(selection.id);
return system ? scaleGalaxyVector(toThreeVector(system.galaxyPosition)) : undefined; return system ? scaleGalaxyVector(toThreeVector(system.galaxyPosition)) : undefined;
} }

View File

@@ -6,6 +6,10 @@ export const NAV_DISTANCE: Record<PovLevel, number> = {
galaxy: 32000, galaxy: 32000,
}; };
// Close-orbit distance when double-clicking a planet (display units).
// 0.005 units = ~333 km from planet center in system space.
export const NAV_DISTANCE_PLANET_ORBIT = 0.005;
export const ACTIVE_SYSTEM_DETAIL_SCALE = 10; export const ACTIVE_SYSTEM_DETAIL_SCALE = 10;
export const GALAXY_PARALLAX_FACTOR = 0.025; export const GALAXY_PARALLAX_FACTOR = 0.025;
export const ACTIVE_SYSTEM_CAPTURE_RADIUS = 9000; export const ACTIVE_SYSTEM_CAPTURE_RADIUS = 9000;
@@ -13,7 +17,8 @@ export const PROJECTED_GALAXY_RADIUS = 65000;
export const STAR_RENDER_SCALE = 0.18; export const STAR_RENDER_SCALE = 0.18;
export const PLANET_RENDER_SCALE = 0.95; export const PLANET_RENDER_SCALE = 0.95;
export const MOON_RENDER_SCALE = 1.1; export const MOON_RENDER_SCALE = 1.1;
export const MIN_CAMERA_DISTANCE = 2; // 0.002 units = ~133 km — allows scrolling into low orbit around planets.
export const MIN_CAMERA_DISTANCE = 0.002;
export const MAX_CAMERA_DISTANCE = 150000; export const MAX_CAMERA_DISTANCE = 150000;
export interface ZoomBlend { export interface ZoomBlend {

View File

@@ -25,15 +25,14 @@ export function createViewerControllers(host: any) {
shipGroup: host.systemLayer.shipGroup, shipGroup: host.systemLayer.shipGroup,
galaxySelectableTargets: host.galaxyLayer.selectableTargets, galaxySelectableTargets: host.galaxyLayer.selectableTargets,
systemSelectableTargets: host.systemLayer.selectableTargets, systemSelectableTargets: host.systemLayer.selectableTargets,
systemVisuals: host.systemVisuals, systemVisuals: host.galaxyLayer.systemVisuals,
planetVisuals: host.planetVisuals, planetVisuals: host.systemLayer.planetVisuals,
orbitLines: host.orbitLines, celestialVisuals: host.systemLayer.celestialVisuals,
celestialVisuals: host.celestialVisuals, nodeVisuals: host.systemLayer.nodeVisuals,
nodeVisuals: host.nodeVisuals, stationVisuals: host.systemLayer.stationVisuals,
stationVisuals: host.stationVisuals, claimVisuals: host.systemLayer.claimVisuals,
claimVisuals: host.claimVisuals, constructionSiteVisuals: host.systemLayer.constructionSiteVisuals,
constructionSiteVisuals: host.constructionSiteVisuals, shipVisuals: host.systemLayer.shipVisuals,
shipVisuals: host.shipVisuals,
}); });
const navigationController = new ViewerNavigationController({ const navigationController = new ViewerNavigationController({
@@ -62,10 +61,10 @@ export function createViewerControllers(host: any) {
systemAnchor: host.systemAnchor, systemAnchor: host.systemAnchor,
galaxyCamera: host.galaxyLayer.camera, galaxyCamera: host.galaxyLayer.camera,
systemCamera: host.systemLayer.camera, systemCamera: host.systemLayer.camera,
shipVisuals: host.shipVisuals, shipVisuals: host.systemLayer.shipVisuals,
nodeVisuals: host.nodeVisuals, nodeVisuals: host.systemLayer.nodeVisuals,
planetVisuals: host.planetVisuals, planetVisuals: host.systemLayer.planetVisuals,
systemVisuals: host.systemVisuals, systemVisuals: host.galaxyLayer.systemVisuals,
followCameraPosition: host.followCameraPosition, followCameraPosition: host.followCameraPosition,
followCameraFocus: host.followCameraFocus, followCameraFocus: host.followCameraFocus,
followCameraDirection: host.followCameraDirection, followCameraDirection: host.followCameraDirection,
@@ -103,9 +102,8 @@ export function createViewerControllers(host: any) {
getSelectedItems: () => host.selectedItems, getSelectedItems: () => host.selectedItems,
getWorldTimeSyncMs: () => host.worldTimeSyncMs, getWorldTimeSyncMs: () => host.worldTimeSyncMs,
getCurrentDistance: () => host.currentDistance, getCurrentDistance: () => host.currentDistance,
planetVisuals: host.planetVisuals, planetVisuals: host.systemLayer.planetVisuals,
orbitLines: host.orbitLines, systemVisuals: host.galaxyLayer.systemVisuals,
systemVisuals: host.systemVisuals,
createWorldPresentationContext: () => host.createWorldPresentationContext(), createWorldPresentationContext: () => host.createWorldPresentationContext(),
}); });
@@ -198,6 +196,7 @@ export function createViewerControllers(host: any) {
galaxySelectableTargets: host.galaxyLayer.selectableTargets, galaxySelectableTargets: host.galaxyLayer.selectableTargets,
systemSelectableTargets: host.systemLayer.selectableTargets, systemSelectableTargets: host.systemLayer.selectableTargets,
hoverLabelEl: host.hoverLabelEl, hoverLabelEl: host.hoverLabelEl,
hoverConnectorLineEl: host.hoverConnectorLineEl,
marqueeEl: host.marqueeEl, marqueeEl: host.marqueeEl,
keyState: host.keyState, keyState: host.keyState,
getWorld: () => host.world, getWorld: () => host.world,

View File

@@ -1,5 +1,5 @@
import * as THREE from "three"; import * as THREE from "three";
import type { Selectable } from "./viewerTypes"; import type { Selectable, SystemVisual } from "./viewerTypes";
/** /**
* Galaxy rendering layer — the galaxy map. * Galaxy rendering layer — the galaxy map.
@@ -15,6 +15,7 @@ export class GalaxyLayer {
readonly systemGroup = new THREE.Group(); readonly systemGroup = new THREE.Group();
readonly selectableTargets = new Map<THREE.Object3D, Selectable>(); readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
readonly systemVisuals = new Map<string, SystemVisual>();
constructor() { constructor() {
this.scene.fog = new THREE.FogExp2(0x040912, 0.000035); this.scene.fog = new THREE.FogExp2(0x040912, 0.000035);
@@ -34,4 +35,8 @@ export class GalaxyLayer {
this.camera.aspect = aspect; this.camera.aspect = aspect;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
} }
render(renderer: THREE.WebGLRenderer) {
renderer.render(this.scene, this.camera);
}
} }

View File

@@ -19,6 +19,7 @@ export interface ViewerHudElements {
historyLayerEl: HTMLDivElement; historyLayerEl: HTMLDivElement;
marqueeEl: HTMLDivElement; marqueeEl: HTMLDivElement;
hoverLabelEl: HTMLDivElement; hoverLabelEl: HTMLDivElement;
hoverConnectorLineEl: SVGLineElement;
} }
export function createViewerHud(documentRef: Document): ViewerHudElements { export function createViewerHud(documentRef: Document): ViewerHudElements {
@@ -73,6 +74,9 @@ export function createViewerHud(documentRef: Document): ViewerHudElements {
<div class="history-layer"></div> <div class="history-layer"></div>
<section class="ops-strip"></section> <section class="ops-strip"></section>
<div class="marquee-box"></div> <div class="marquee-box"></div>
<svg class="hover-connector-svg" aria-hidden="true">
<line class="hover-connector-line" x1="0" y1="0" x2="0" y2="0" hidden></line>
</svg>
<div class="hover-label" hidden></div> <div class="hover-label" hidden></div>
`; `;
@@ -97,5 +101,6 @@ export function createViewerHud(documentRef: Document): ViewerHudElements {
historyLayerEl: root.querySelector(".history-layer") as HTMLDivElement, historyLayerEl: root.querySelector(".history-layer") as HTMLDivElement,
marqueeEl: root.querySelector(".marquee-box") as HTMLDivElement, marqueeEl: root.querySelector(".marquee-box") as HTMLDivElement,
hoverLabelEl: root.querySelector(".hover-label") as HTMLDivElement, hoverLabelEl: root.querySelector(".hover-label") as HTMLDivElement,
hoverConnectorLineEl: root.querySelector(".hover-connector-line") as unknown as SVGLineElement,
}; };
} }

View File

@@ -68,6 +68,7 @@ export function pickSelectableHitAtClientPosition(
export function updateHoverLabel(params: { export function updateHoverLabel(params: {
dragMode?: string; dragMode?: string;
hoverLabelEl: HTMLDivElement; hoverLabelEl: HTMLDivElement;
hoverConnectorLineEl: SVGLineElement;
hoverPick: HoverPickResult | undefined; hoverPick: HoverPickResult | undefined;
activeSystemId?: string; activeSystemId?: string;
povLevel: PovLevel; povLevel: PovLevel;
@@ -77,6 +78,7 @@ export function updateHoverLabel(params: {
const { const {
dragMode, dragMode,
hoverLabelEl, hoverLabelEl,
hoverConnectorLineEl,
hoverPick, hoverPick,
activeSystemId, activeSystemId,
povLevel, povLevel,
@@ -84,13 +86,9 @@ export function updateHoverLabel(params: {
point, point,
} = params; } = params;
if (dragMode) { if (dragMode || !hoverPick) {
hoverLabelEl.hidden = true;
return;
}
if (!hoverPick) {
hoverLabelEl.hidden = true; hoverLabelEl.hidden = true;
hoverConnectorLineEl.setAttribute("hidden", "");
return; return;
} }
@@ -98,6 +96,7 @@ export function updateHoverLabel(params: {
const label = describeHoverLabel(world, selection); const label = describeHoverLabel(world, selection);
if (!label) { if (!label) {
hoverLabelEl.hidden = true; hoverLabelEl.hidden = true;
hoverConnectorLineEl.setAttribute("hidden", "");
return; return;
} }
@@ -105,8 +104,16 @@ export function updateHoverLabel(params: {
hoverLabelEl.hidden = false; hoverLabelEl.hidden = false;
hoverLabelEl.textContent = `${label}\n${distance}`; hoverLabelEl.textContent = `${label}\n${distance}`;
hoverLabelEl.style.left = `${point.x + 14}px`; hoverLabelEl.style.left = `${point.x + 44}px`;
hoverLabelEl.style.top = `${point.y + 14}px`; hoverLabelEl.style.top = `${point.y - 90}px`;
const rect = hoverLabelEl.getBoundingClientRect();
const svgRect = (hoverConnectorLineEl.ownerSVGElement as SVGSVGElement).getBoundingClientRect();
hoverConnectorLineEl.removeAttribute("hidden");
hoverConnectorLineEl.setAttribute("x1", String(point.x));
hoverConnectorLineEl.setAttribute("y1", String(point.y));
hoverConnectorLineEl.setAttribute("x2", String(rect.left - svgRect.left));
hoverConnectorLineEl.setAttribute("y2", String(rect.top - svgRect.top + rect.height / 2));
} }
function formatHoverDistance( function formatHoverDistance(

View File

@@ -12,6 +12,7 @@ import {
toggleCameraMode, toggleCameraMode,
navigateFromWheel, navigateFromWheel,
} from "./viewerControls"; } from "./viewerControls";
import { NAV_DISTANCE, NAV_DISTANCE_PLANET_ORBIT } from "./viewerConstants";
import { ViewerHistoryWindowController } from "./viewerHistoryWindowController"; import { ViewerHistoryWindowController } from "./viewerHistoryWindowController";
import type { import type {
CameraMode, CameraMode,
@@ -30,6 +31,7 @@ export interface ViewerInteractionContext {
galaxySelectableTargets: Map<THREE.Object3D, Selectable>; galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
systemSelectableTargets: Map<THREE.Object3D, Selectable>; systemSelectableTargets: Map<THREE.Object3D, Selectable>;
hoverLabelEl: HTMLDivElement; hoverLabelEl: HTMLDivElement;
hoverConnectorLineEl: SVGLineElement;
marqueeEl: HTMLDivElement; marqueeEl: HTMLDivElement;
keyState: Set<string>; keyState: Set<string>;
getWorld: () => WorldState | undefined; getWorld: () => WorldState | undefined;
@@ -231,8 +233,12 @@ export class ViewerInteractionController {
return; return;
} }
this.context.focusOnSelection(selectedItems[0]); const selection = selectedItems[0];
this.context.focusOnSelection(selection);
this.context.syncFollowStateFromSelection(); this.context.syncFollowStateFromSelection();
if (selection.kind === "planet") {
this.context.setDesiredDistance(NAV_DISTANCE_PLANET_ORBIT);
}
}; };
readonly onWheel = (event: WheelEvent) => { readonly onWheel = (event: WheelEvent) => {
@@ -269,6 +275,7 @@ export class ViewerInteractionController {
updateHoverLabel({ updateHoverLabel({
dragMode: this.context.getDragMode(), dragMode: this.context.getDragMode(),
hoverLabelEl: this.context.hoverLabelEl, hoverLabelEl: this.context.hoverLabelEl,
hoverConnectorLineEl: this.context.hoverConnectorLineEl,
hoverPick: this.pickSelectableHitAtClientPosition(event.clientX, event.clientY), hoverPick: this.pickSelectableHitAtClientPosition(event.clientX, event.clientY),
activeSystemId: this.context.getActiveSystemId(), activeSystemId: this.context.getActiveSystemId(),
povLevel: this.context.getPovLevel(), povLevel: this.context.getPovLevel(),

View File

@@ -21,4 +21,8 @@ export class LocalLayer {
this.camera.aspect = aspect; this.camera.aspect = aspect;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
} }
render(renderer: THREE.WebGLRenderer) {
renderer.render(this.scene, this.camera);
}
} }

View File

@@ -3,6 +3,7 @@ import { MOON_RENDER_SCALE } from "./viewerConstants";
import type { import type {
ShipSnapshot, ShipSnapshot,
PlanetSnapshot, PlanetSnapshot,
MoonSnapshot,
Vector3Dto, Vector3Dto,
WorldSnapshot, WorldSnapshot,
} from "./contracts"; } from "./contracts";
@@ -176,7 +177,7 @@ export function computePlanetLocalPosition(planet: PlanetSnapshot, timeSeconds:
const eccentricAnomaly = meanAnomaly const eccentricAnomaly = meanAnomaly
+ (eccentricity * Math.sin(meanAnomaly)) + (eccentricity * Math.sin(meanAnomaly))
+ (0.5 * eccentricity * eccentricity * Math.sin(2 * meanAnomaly)); + (0.5 * eccentricity * eccentricity * Math.sin(2 * meanAnomaly));
const semiMajorAxis = planet.orbitRadius; const semiMajorAxis = planet.orbitRadius * KILOMETERS_PER_AU;
const semiMinorAxis = semiMajorAxis * Math.sqrt(Math.max(1 - (eccentricity * eccentricity), 0.05)); const semiMinorAxis = semiMajorAxis * Math.sqrt(Math.max(1 - (eccentricity * eccentricity), 0.05));
const local = new THREE.Vector3( const local = new THREE.Vector3(
semiMajorAxis * (Math.cos(eccentricAnomaly) - eccentricity), semiMajorAxis * (Math.cos(eccentricAnomaly) - eccentricity),
@@ -190,47 +191,24 @@ export function computePlanetLocalPosition(planet: PlanetSnapshot, timeSeconds:
return local; return local;
} }
export function computeMoonOrbitRadius(planet: PlanetSnapshot, moonIndex: number, seed: number): number { export function computeMoonLocalPosition(moon: MoonSnapshot, timeSeconds: number): THREE.Vector3 {
const spacing = planet.size * 1.4; const angle = THREE.MathUtils.degToRad(moon.orbitPhaseAtEpoch) + (timeSeconds * moon.orbitSpeed);
const variance = hashUnit(seed, `${planet.label}:${moonIndex}:radius`) * planet.size * 0.9;
return (planet.size * 1.8) + (moonIndex * spacing) + variance;
}
export function computeMoonOrbitSpeed(planet: PlanetSnapshot, moonIndex: number, seed: number): number {
const radius = computeMoonOrbitRadius(planet, moonIndex, seed);
return 0.9 / Math.sqrt(Math.max(radius, 1)) + (moonIndex * 0.003);
}
export function computeMoonLocalPosition(planet: PlanetSnapshot, moonIndex: number, timeSeconds: number, seed: number): THREE.Vector3 {
const orbitRadius = computeMoonOrbitRadius(planet, moonIndex, seed);
const speed = computeMoonOrbitSpeed(planet, moonIndex, seed);
const phase = hashUnit(seed, `${planet.label}:${moonIndex}:phase`) * Math.PI * 2;
const inclination = THREE.MathUtils.degToRad((hashUnit(seed, `${planet.label}:${moonIndex}:inclination`) - 0.5) * 28);
const node = THREE.MathUtils.degToRad(hashUnit(seed, `${planet.label}:${moonIndex}:node`) * 360);
const angle = phase + (timeSeconds * speed);
const local = new THREE.Vector3( const local = new THREE.Vector3(
Math.cos(angle) * orbitRadius, Math.cos(angle) * moon.orbitRadius,
0, 0,
Math.sin(angle) * orbitRadius, Math.sin(angle) * moon.orbitRadius,
); );
local.applyAxisAngle(new THREE.Vector3(1, 0, 0), inclination); local.applyAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(moon.orbitInclination));
local.applyAxisAngle(new THREE.Vector3(0, 1, 0), node); local.applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(moon.orbitLongitudeOfAscendingNode));
return local; return local;
} }
export function computeMoonSize(planet: PlanetSnapshot, moonIndex: number, seed: number): number {
const base = Math.max(2.2, planet.size * 0.11);
const variance = hashUnit(seed, `${planet.label}:${moonIndex}:size`) * Math.max(planet.size * 0.16, 2.5);
return Math.min(base + variance, planet.size * 0.42);
}
export function celestialRenderRadius(size: number, scale: number, minRadius: number, exponent = 1): number { export function celestialRenderRadius(size: number, scale: number, minRadius: number, exponent = 1): number {
return Math.max(minRadius, Math.pow(Math.max(size, 0.1), exponent) * scale); return Math.max(minRadius, Math.pow(Math.max(size, 0.1), exponent) * scale);
} }
export function computeMoonRenderRadius(planet: PlanetSnapshot, moonIndex: number, seed: number): number { export function computeMoonRenderRadius(moon: MoonSnapshot): number {
return celestialRenderRadius(computeMoonSize(planet, moonIndex, seed), 0.00011, 0.025, 0.62); return celestialRenderRadius(moon.size, 0.00011, 0.025, 0.62);
} }
export function starHaloOpacity(starKind: string): number { export function starHaloOpacity(starKind: string): number {
@@ -251,7 +229,6 @@ export function resolveOrbitalAnchorPosition(
systemId: string, systemId: string,
anchor: OrbitalAnchor, anchor: OrbitalAnchor,
timeSeconds: number, timeSeconds: number,
seed: number,
): THREE.Vector3 { ): THREE.Vector3 {
if (!world || anchor.kind === "star") { if (!world || anchor.kind === "star") {
return new THREE.Vector3(); return new THREE.Vector3();
@@ -268,5 +245,6 @@ export function resolveOrbitalAnchorPosition(
return planetPosition; return planetPosition;
} }
return planetPosition.add(computeMoonLocalPosition(planet, anchor.moonIndex, timeSeconds, seed)); const moon = planet.moons[anchor.moonIndex];
return planetPosition.add(computeMoonLocalPosition(moon, timeSeconds));
} }

View File

@@ -351,13 +351,27 @@ export function updateDetailPanel(
detailBodyEl.innerHTML = ` detailBodyEl.innerHTML = `
<p>${system.label}</p> <p>${system.label}</p>
<p>Parent ${parent}</p> <p>Parent ${parent}</p>
<p>${planet.planetType} · ${planet.shape} · Moons ${planet.moonCount}</p> <p>${planet.planetType} · ${planet.shape} · Moons ${planet.moons.length}</p>
<p>Orbit ${formatSystemDistance(planet.orbitRadius)}<br>Speed ${planet.orbitSpeed.toFixed(3)}<br>Ecc ${planet.orbitEccentricity.toFixed(3)}<br>Inc ${planet.orbitInclination.toFixed(1)}°</p> <p>Orbit ${formatSystemDistance(planet.orbitRadius)}<br>Speed ${planet.orbitSpeed.toFixed(3)}<br>Ecc ${planet.orbitEccentricity.toFixed(3)}<br>Inc ${planet.orbitInclination.toFixed(1)}°</p>
<p>Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°</p> <p>Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°</p>
`; `;
return; return;
} }
if (selected.kind === "moon") {
const system = world.systems.get(selected.systemId);
const planet = system?.planets[selected.planetIndex];
const moon = planet?.moons[selected.moonIndex];
if (moon) {
detailTitleEl.textContent = moon.label;
detailBodyEl.innerHTML = `
<p>${system?.label ?? selected.systemId} / ${planet?.label ?? `planet ${selected.planetIndex + 1}`}</p>
<p>Orbit ${formatSystemDistance(moon.orbitRadius)}<br>Inc ${moon.orbitInclination.toFixed(1)}°</p>
`;
}
return;
}
const system = world.systems.get(selected.id); const system = world.systems.get(selected.id);
if (!system) { if (!system) {
return; return;

View File

@@ -2,6 +2,14 @@ import * as THREE from "three";
import { ACTIVE_SYSTEM_DETAIL_SCALE, PROJECTED_GALAXY_RADIUS } from "./viewerConstants"; import { ACTIVE_SYSTEM_DETAIL_SCALE, PROJECTED_GALAXY_RADIUS } from "./viewerConstants";
import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeSeconds, scaleLocalVector } from "./viewerMath"; import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeSeconds, scaleLocalVector } from "./viewerMath";
import type { PlanetVisual, ShipVisual, SystemVisual, WorldState } from "./viewerTypes"; import type { PlanetVisual, ShipVisual, SystemVisual, WorldState } from "./viewerTypes";
import { rawObject } from "./viewerScenePrimitives";
const MIN_ICON_PIXELS = 25;
const MAX_ICON_PIXELS = 50;
export function iconWorldScale(distToCamera: number, camera: THREE.PerspectiveCamera, pixels: number): number {
return pixels * distToCamera * 2 * Math.tan((camera.fov * Math.PI / 180) / 2) / window.innerHeight;
}
export function getAnimatedShipLocalPosition(visual: ShipVisual, now = performance.now()) { export function getAnimatedShipLocalPosition(visual: ShipVisual, now = performance.now()) {
const elapsedMs = now - visual.receivedAtMs; const elapsedMs = now - visual.receivedAtMs;
@@ -26,6 +34,7 @@ export function updatePlanetPresentation(
world: WorldState | undefined, world: WorldState | undefined,
worldTimeSyncMs: number, worldTimeSyncMs: number,
planetVisuals: PlanetVisual[], planetVisuals: PlanetVisual[],
systemCamera: THREE.PerspectiveCamera,
) { ) {
const nowSeconds = currentWorldTimeSeconds(world, worldTimeSyncMs); const nowSeconds = currentWorldTimeSeconds(world, worldTimeSyncMs);
// In systemScene all positions use scaleLocalVector * ACTIVE_SYSTEM_DETAIL_SCALE. // In systemScene all positions use scaleLocalVector * ACTIVE_SYSTEM_DETAIL_SCALE.
@@ -34,23 +43,44 @@ export function updatePlanetPresentation(
const position = scaleLocalVector(computePlanetLocalPosition(visual.planet, nowSeconds)) const position = scaleLocalVector(computePlanetLocalPosition(visual.planet, nowSeconds))
.multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE); .multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE);
visual.orbit.setScaleScalar(ACTIVE_SYSTEM_DETAIL_SCALE);
visual.orbit.setPosition(new THREE.Vector3(0, 0, 0));
visual.mesh.setPosition(position); visual.mesh.setPosition(position);
visual.icon.setPosition(position); visual.icon.setPosition(position);
const iconWorldPos = visual.icon.getWorldPosition(new THREE.Vector3());
const distToIcon = systemCamera.position.distanceTo(iconWorldPos);
const t = THREE.MathUtils.clamp(distToIcon / 300, 0, 1);
const rawScale = visual.iconBaseScale * t * Math.sqrt(t);
const planetIconScale = THREE.MathUtils.clamp(rawScale, iconWorldScale(distToIcon, systemCamera, MIN_ICON_PIXELS), iconWorldScale(distToIcon, systemCamera, MAX_ICON_PIXELS));
visual.icon.setScaleScalar(planetIconScale);
if (visual.ring) { if (visual.ring) {
visual.ring.setPosition(position); visual.ring.setPosition(position);
} }
const distToPlanet = systemCamera.position.distanceTo(position);
const moonOrbitOpacity = THREE.MathUtils.clamp(1 - distToPlanet / 500, 0, 1) * 0.18;
const clusterVisible = distToPlanet < 300;
for (const [moonIndex, moon] of visual.moons.entries()) { for (const [moonIndex, moon] of visual.moons.entries()) {
moon.orbit.setPosition(position); const moonPos = position.clone().add(
moon.orbit.setScaleScalar(ACTIVE_SYSTEM_DETAIL_SCALE); scaleLocalVector(computeMoonLocalPosition(visual.planet.moons[moonIndex], nowSeconds))
moon.mesh.setPosition( .multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE),
position.clone().add(
scaleLocalVector(computeMoonLocalPosition(visual.planet, moonIndex, nowSeconds, world?.seed ?? 1))
.multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE),
),
); );
moon.mesh.setPosition(moonPos);
moon.mesh.setVisible(clusterVisible);
moon.icon.setPosition(moonPos);
moon.icon.setVisible(clusterVisible);
if (clusterVisible) {
const iconWorldPos = moon.icon.getWorldPosition(new THREE.Vector3());
const moonDist = systemCamera.position.distanceTo(iconWorldPos);
const t = THREE.MathUtils.clamp(moonDist / 120, 0, 1);
const rawMoonScale = moon.iconBaseScale * t * Math.sqrt(t);
const moonIconScale = THREE.MathUtils.clamp(rawMoonScale, iconWorldScale(moonDist, systemCamera, MIN_ICON_PIXELS), iconWorldScale(moonDist, systemCamera, MAX_ICON_PIXELS));
moon.icon.setScaleScalar(moonIconScale);
}
moon.orbit.setPosition(position);
const orbitObj = rawObject(moon.orbit);
if (orbitObj instanceof THREE.LineLoop) {
(orbitObj.material as THREE.LineBasicMaterial).opacity = moonOrbitOpacity;
}
} }
} }
} }

View File

@@ -9,8 +9,8 @@ import {
import { updatePlanetPresentation } from "./viewerPresentation"; import { updatePlanetPresentation } from "./viewerPresentation";
import { renderRecentEvents, updateGameStatus, updateSystemSummaries, updateWorldPresentation } from "./viewerWorldPresentation"; import { renderRecentEvents, updateGameStatus, updateSystemSummaries, updateWorldPresentation } from "./viewerWorldPresentation";
import { updateSystemPanel } from "./viewerPanels"; import { updateSystemPanel } from "./viewerPanels";
import { createBackdropStars, createNebulaClouds, createNebulaTexture } from "./viewerSceneFactory"; import { createBackdropStars, createMilkyWayBand, createNebulaClouds, createNebulaTexture } from "./viewerSceneFactory";
import type { OrbitLineVisual, Selectable } from "./viewerTypes"; import type { Selectable } from "./viewerTypes";
export interface ViewerPresentationContext { export interface ViewerPresentationContext {
renderer: THREE.WebGLRenderer; renderer: THREE.WebGLRenderer;
@@ -40,7 +40,6 @@ export interface ViewerPresentationContext {
getWorldTimeSyncMs: () => number; getWorldTimeSyncMs: () => number;
getCurrentDistance: () => number; getCurrentDistance: () => number;
planetVisuals: any[]; planetVisuals: any[];
orbitLines: OrbitLineVisual[];
systemVisuals: Map<any, any>; systemVisuals: Map<any, any>;
createWorldPresentationContext: () => any; createWorldPresentationContext: () => any;
} }
@@ -50,30 +49,28 @@ export class ViewerPresentationController {
initializeAmbience() { initializeAmbience() {
this.context.ambienceGroup.renderOrder = -10; this.context.ambienceGroup.renderOrder = -10;
this.context.ambienceGroup.add(createBackdropStars()); this.context.ambienceGroup.add(createBackdropStars(document));
this.context.ambienceGroup.add(...createNebulaClouds(createNebulaTexture(document))); this.context.ambienceGroup.add(...createNebulaClouds(createNebulaTexture(document)));
this.context.ambienceGroup.add(createMilkyWayBand(document));
} }
updateAmbience(delta: number) { updateAmbience(_delta: number) {
const activeCamera = this.context.getPovLevel() === "galaxy" const activeCamera = this.context.getPovLevel() === "galaxy"
? this.context.galaxyCamera ? this.context.galaxyCamera
: this.context.systemCamera; : this.context.systemCamera;
this.context.ambienceGroup.position.copy(activeCamera.position); this.context.ambienceGroup.position.copy(activeCamera.position);
this.context.ambienceGroup.rotation.y += delta * 0.005;
this.context.ambienceGroup.rotation.x = Math.sin(performance.now() * 0.00003) * 0.015;
} }
applyZoomPresentation() { applyZoomPresentation() {
const activeSystemId = this.context.getActiveSystemId(); const activeSystemId = this.context.getActiveSystemId();
const povLevel = this.context.getPovLevel(); const povLevel = this.context.getPovLevel();
// Orbit lines: only show for active system in system/local zoom
for (const orbitLine of this.context.orbitLines) {
const alpha = this.resolveOrbitLineOpacity(orbitLine, povLevel, activeSystemId);
orbitLine.line.setOpacity(alpha);
}
this.context.galaxyScene.fog = new THREE.FogExp2(0x040912, 0.000035); this.context.galaxyScene.fog = new THREE.FogExp2(0x040912, 0.000035);
const showPlanetIcons = povLevel !== "local";
for (const visual of this.context.planetVisuals) {
visual.icon.setVisible(showPlanetIcons);
}
} }
updateNetworkPanel() { updateNetworkPanel() {
@@ -100,6 +97,7 @@ export class ViewerPresentationController {
world, world,
this.context.getWorldTimeSyncMs(), this.context.getWorldTimeSyncMs(),
this.context.planetVisuals, this.context.planetVisuals,
this.context.systemCamera,
); );
} }
@@ -148,21 +146,4 @@ export class ViewerPresentationController {
return new THREE.Vector2(clientX - bounds.left, clientY - bounds.top); return new THREE.Vector2(clientX - bounds.left, clientY - bounds.top);
} }
private resolveOrbitLineOpacity(orbitLine: OrbitLineVisual, povLevel: "local" | "system" | "galaxy", activeSystemId?: string) {
if (povLevel === "galaxy" || !activeSystemId || orbitLine.systemId !== activeSystemId) {
return 0;
}
const selected = this.context.getSelectedItems();
const selectedItem = selected.length === 1 ? selected[0] : undefined;
const baseAlpha = povLevel === "local" ? 0.55 : 0.9;
if (selectedItem?.kind === "planet" && selectedItem.systemId === activeSystemId) {
return orbitLine.kind === "moon" && orbitLine.planetIndex === selectedItem.planetIndex
? baseAlpha
: 0;
}
return orbitLine.kind === "planet" ? baseAlpha : 0;
}
} }

View File

@@ -1,17 +1,18 @@
import * as THREE from "three"; import * as THREE from "three";
import { classifyPovLevel } from "./viewerMath"; import { classifyPovLevel } from "./viewerMath";
import type { PovLevel, PerformanceStats } from "./viewerTypes"; import type { PovLevel } from "./viewerTypes";
import type { UniverseLayer } from "./viewerUniverseLayer";
import type { GalaxyLayer } from "./viewerGalaxyLayer";
import type { SystemLayer } from "./viewerSystemLayer";
import type { LocalLayer } from "./viewerLocalLayer";
export interface RenderFrameParams { export interface RenderFrameParams {
clock: THREE.Clock; clock: THREE.Clock;
renderer: THREE.WebGLRenderer; renderer: THREE.WebGLRenderer;
universeScene: THREE.Scene; universeLayer: UniverseLayer;
galaxyScene: THREE.Scene; galaxyLayer: GalaxyLayer;
galaxyCamera: THREE.PerspectiveCamera; systemLayer: SystemLayer;
systemScene: THREE.Scene; localLayer: LocalLayer;
systemCamera: THREE.PerspectiveCamera;
localScene: THREE.Scene;
localCamera: THREE.PerspectiveCamera;
getPovLevel: () => PovLevel; getPovLevel: () => PovLevel;
updateCamera: (delta: number) => void; updateCamera: (delta: number) => void;
updateAmbience: (delta: number) => void; updateAmbience: (delta: number) => void;
@@ -25,9 +26,9 @@ export interface RenderFrameParams {
export interface ResizeParams { export interface ResizeParams {
renderer: THREE.WebGLRenderer; renderer: THREE.WebGLRenderer;
galaxyCamera: THREE.PerspectiveCamera; galaxyLayer: GalaxyLayer;
systemCamera: THREE.PerspectiveCamera; systemLayer: SystemLayer;
localCamera: THREE.PerspectiveCamera; localLayer: LocalLayer;
} }
export interface CameraStepParams { export interface CameraStepParams {
@@ -48,22 +49,22 @@ export function renderFrame(params: RenderFrameParams) {
params.applyZoomPresentation(); params.applyZoomPresentation();
const povLevel = params.getPovLevel(); const povLevel = params.getPovLevel();
const activeCamera = povLevel === "galaxy" ? params.galaxyCamera : params.systemCamera; const activeCamera = povLevel === "galaxy" ? params.galaxyLayer.camera : params.systemLayer.camera;
params.renderer.autoClear = false; params.renderer.autoClear = false;
params.renderer.clear(); params.renderer.clear();
// Universe backdrop — always first, rendered with the active camera so it aligns with the foreground // Universe backdrop — always first, rendered with the active camera so it aligns with the foreground
params.renderer.render(params.universeScene, activeCamera); params.universeLayer.render(params.renderer, activeCamera);
params.renderer.clearDepth(); params.renderer.clearDepth();
if (povLevel === "galaxy") { if (povLevel === "galaxy") {
// Galaxy map on top of universe backdrop // Galaxy map on top of universe backdrop
params.renderer.render(params.galaxyScene, params.galaxyCamera); params.galaxyLayer.render(params.renderer);
} else if (povLevel === "system") { } else if (povLevel === "system") {
params.renderer.render(params.systemScene, params.systemCamera); params.systemLayer.render(params.renderer);
} else { } else {
// local: system as mid-ground backdrop, then local on top // local: system as mid-ground backdrop, then local on top
params.renderer.render(params.systemScene, params.systemCamera); params.systemLayer.render(params.renderer);
params.renderer.clearDepth(); params.renderer.clearDepth();
params.renderer.render(params.localScene, params.localCamera); params.localLayer.render(params.renderer);
} }
params.recordPerformanceStats(performance.now() - frameStartedAtMs); params.recordPerformanceStats(performance.now() - frameStartedAtMs);
@@ -73,10 +74,9 @@ export function renderFrame(params: RenderFrameParams) {
export function resizeViewer(params: ResizeParams) { export function resizeViewer(params: ResizeParams) {
const width = window.innerWidth; const width = window.innerWidth;
const height = window.innerHeight; const height = window.innerHeight;
for (const camera of [params.galaxyCamera, params.systemCamera, params.localCamera]) { params.galaxyLayer.onResize(width / height);
camera.aspect = width / height; params.systemLayer.onResize(width / height);
camera.updateProjectionMatrix(); params.localLayer.onResize(width / height);
}
params.renderer.setSize(width, height); params.renderer.setSize(width, height);
} }

View File

@@ -42,7 +42,7 @@ import type {
StationSnapshot, StationSnapshot,
SystemSnapshot, SystemSnapshot,
} from "./contracts"; } from "./contracts";
import type { OrbitLineVisual, OrbitalAnchor, Selectable } from "./viewerTypes"; import type { OrbitalAnchor, Selectable } from "./viewerTypes";
import { rawObject } from "./viewerScenePrimitives"; import { rawObject } from "./viewerScenePrimitives";
export interface ViewerSceneDataContext { export interface ViewerSceneDataContext {
@@ -65,7 +65,6 @@ export interface ViewerSceneDataContext {
systemSelectableTargets: Map<THREE.Object3D, Selectable>; systemSelectableTargets: Map<THREE.Object3D, Selectable>;
systemVisuals: Map<any, any>; systemVisuals: Map<any, any>;
planetVisuals: any[]; planetVisuals: any[];
orbitLines: OrbitLineVisual[];
celestialVisuals: Map<any, any>; celestialVisuals: Map<any, any>;
nodeVisuals: Map<any, any>; nodeVisuals: Map<any, any>;
stationVisuals: Map<any, any>; stationVisuals: Map<any, any>;
@@ -251,7 +250,6 @@ export class ViewerSceneDataController {
systemSelectableTargets: this.context.systemSelectableTargets, systemSelectableTargets: this.context.systemSelectableTargets,
systemVisuals: this.context.systemVisuals, systemVisuals: this.context.systemVisuals,
planetVisuals: this.context.planetVisuals, planetVisuals: this.context.planetVisuals,
orbitLines: this.context.orbitLines,
celestialVisuals: this.context.celestialVisuals, celestialVisuals: this.context.celestialVisuals,
nodeVisuals: this.context.nodeVisuals, nodeVisuals: this.context.nodeVisuals,
stationVisuals: this.context.stationVisuals, stationVisuals: this.context.stationVisuals,

View File

@@ -1,5 +1,6 @@
import * as THREE from "three"; import * as THREE from "three";
import { import {
ACTIVE_SYSTEM_DETAIL_SCALE,
MOON_RENDER_SCALE, MOON_RENDER_SCALE,
PLANET_RENDER_SCALE, PLANET_RENDER_SCALE,
STAR_RENDER_SCALE, STAR_RENDER_SCALE,
@@ -8,6 +9,7 @@ import type {
CelestialSnapshot, CelestialSnapshot,
ClaimSnapshot, ClaimSnapshot,
ConstructionSiteSnapshot, ConstructionSiteSnapshot,
MoonSnapshot,
PlanetSnapshot, PlanetSnapshot,
ResourceNodeSnapshot, ResourceNodeSnapshot,
ShipSnapshot, ShipSnapshot,
@@ -17,10 +19,9 @@ import type {
import type { MoonVisual } from "./viewerTypes"; import type { MoonVisual } from "./viewerTypes";
import { import {
celestialRenderRadius, celestialRenderRadius,
computeMoonOrbitRadius, computeMoonLocalPosition,
computeMoonRenderRadius, computeMoonRenderRadius,
computePlanetLocalPosition, computePlanetLocalPosition,
scaleLocalScalar,
scaleLocalVector, scaleLocalVector,
starHaloOpacity, starHaloOpacity,
toThreeVector, toThreeVector,
@@ -84,45 +85,34 @@ export function createConstructionSiteMesh(site: ConstructionSiteSnapshot): Scen
export function createStarCluster(system: SystemSnapshot): SceneNode { export function createStarCluster(system: SystemSnapshot): SceneNode {
const root = new THREE.Group(); const root = new THREE.Group();
const renderedStarSize = celestialRenderRadius(system.starSize, 0.00018, 0.16, 0.62); for (const [index, star] of system.stars.entries()) {
const offsets = system.starCount > 1 const renderedSize = celestialRenderRadius(star.size, 0.00018, 40, 0.62);
? [new THREE.Vector3(-renderedStarSize * 0.55, 0, 0), new THREE.Vector3(renderedStarSize * 0.75, renderedStarSize * 0.08, 0)] const offset = system.stars.length > 1
: [new THREE.Vector3(0, 0, 0)]; ? (index === 0
? new THREE.Vector3(-renderedSize * 0.55, 0, 0)
for (const [index, offset] of offsets.entries()) { : new THREE.Vector3(renderedSize * 0.75, renderedSize * 0.08, 0))
const sizeScale = index === 0 ? 1 : 0.72; : new THREE.Vector3(0, 0, 0);
const star = new THREE.Mesh( const mesh = new THREE.Mesh(
new THREE.SphereGeometry(renderedStarSize * sizeScale, 24, 24), new THREE.SphereGeometry(renderedSize, 24, 24),
new THREE.MeshBasicMaterial({ color: system.starColor }), new THREE.MeshBasicMaterial({ color: star.color }),
); );
const halo = new THREE.Mesh( const halo = new THREE.Mesh(
new THREE.SphereGeometry(renderedStarSize * sizeScale * 1.45, 20, 20), new THREE.SphereGeometry(renderedSize * 1.45, 20, 20),
new THREE.MeshBasicMaterial({ new THREE.MeshBasicMaterial({
color: system.starColor, color: star.color,
transparent: true, transparent: true,
opacity: starHaloOpacity(system.starKind), opacity: starHaloOpacity(star.kind),
side: THREE.BackSide, side: THREE.BackSide,
}), }),
); );
star.position.copy(offset); mesh.position.copy(offset);
halo.position.copy(offset); halo.position.copy(offset);
root.add(star, halo); root.add(mesh, halo);
} }
return createSceneNode(root); return createSceneNode(root);
} }
export function createPlanetOrbit(planet: PlanetSnapshot): SceneNode {
const points = Array.from({ length: 120 }, (_, index) => {
const phaseDegrees = (index / 120) * 360;
return scaleLocalVector(computePlanetLocalPosition(planet, 0, phaseDegrees));
});
return createSceneNode(new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(points),
new THREE.LineBasicMaterial({ color: 0x17314d, transparent: true, opacity: 0.22 }),
));
}
export function createPlanetRing(planet: PlanetSnapshot): SceneNode { export function createPlanetRing(planet: PlanetSnapshot): SceneNode {
const renderedPlanetRadius = celestialRenderRadius(planet.size, 0.00012, 0.03, 0.62); const renderedPlanetRadius = celestialRenderRadius(planet.size, 0.00012, 0.03, 0.62);
@@ -140,41 +130,74 @@ export function createPlanetRing(planet: PlanetSnapshot): SceneNode {
return createSceneNode(ring); return createSceneNode(ring);
} }
export function createMoonVisuals(planet: PlanetSnapshot, seed: number): MoonVisual[] { function createMoonOrbit(moon: MoonSnapshot): SceneNode {
const moonCount = Math.min(planet.moonCount, 12); const segments = 64;
const moons: MoonVisual[] = []; const period = (2 * Math.PI) / Math.max(Math.abs(moon.orbitSpeed), 1e-6);
const points: THREE.Vector3[] = [];
for (let moonIndex = 0; moonIndex < moonCount; moonIndex += 1) { for (let i = 0; i <= segments; i++) {
const orbitRadius = scaleLocalScalar(computeMoonOrbitRadius(planet, moonIndex, seed)); points.push(
const orbit = new THREE.LineLoop( scaleLocalVector(computeMoonLocalPosition(moon, (i / segments) * period))
new THREE.BufferGeometry().setFromPoints( .multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE),
Array.from({ length: 48 }, (_, index) => {
const angle = (index / 48) * Math.PI * 2;
return new THREE.Vector3(
Math.cos(angle) * orbitRadius,
0,
Math.sin(angle) * orbitRadius,
);
}),
),
new THREE.LineBasicMaterial({ color: 0x3b5065, transparent: true, opacity: 0.1 }),
); );
orbit.rotation.x = THREE.MathUtils.degToRad(planet.orbitInclination * 0.35); }
return createSceneNode(new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(points),
new THREE.LineBasicMaterial({
color: moon.color,
transparent: true,
opacity: 0,
depthWrite: false,
depthTest: false,
}),
));
}
const moonSize = computeMoonRenderRadius(planet, moonIndex, seed); export function createMoonVisuals(planet: PlanetSnapshot, documentRef: Document): MoonVisual[] {
return planet.moons.map((moon, moonIndex) => {
const moonSize = computeMoonRenderRadius(moon);
const mesh = new THREE.Mesh( const mesh = new THREE.Mesh(
new THREE.SphereGeometry(moonSize, 12, 12), new THREE.SphereGeometry(moonSize, 12, 12),
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: new THREE.Color(planet.color).lerp(new THREE.Color("#d9dee7"), 0.55), color: moon.color,
roughness: 0.96, roughness: 0.96,
metalness: 0.02, metalness: 0.02,
}), }),
); );
const baseColor = new THREE.Color(moon.color);
const hsl = { h: 0, s: 0, l: 0 };
baseColor.getHSL(hsl);
const iconColor = new THREE.Color().setHSL(hsl.h, Math.max(hsl.s, 0.4), 0.72).getStyle();
const iconBaseScale = 72;
const icon = createTacticalIcon(documentRef, iconColor, iconBaseScale);
return {
systemId: "",
planetIndex: -1,
moonIndex,
mesh: createSceneNode(mesh),
icon,
iconBaseScale,
orbit: createMoonOrbit(moon),
};
});
}
moons.push({ systemId: "", planetIndex: -1, mesh: createSceneNode(mesh), orbit: createSceneNode(orbit) }); export function createPlanetOrbit(planet: PlanetSnapshot): SceneNode {
const segments = 96;
const points: THREE.Vector3[] = [];
for (let i = 0; i <= segments; i++) {
const phase = (i / segments) * 360;
points.push(scaleLocalVector(computePlanetLocalPosition(planet, 0, phase)).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE));
} }
return createSceneNode(new THREE.LineLoop(
return moons; new THREE.BufferGeometry().setFromPoints(points),
new THREE.LineBasicMaterial({
color: planet.color,
transparent: true,
opacity: 0.22,
depthWrite: false,
depthTest: false,
}),
));
} }
export function createStationMesh(station: StationSnapshot): SceneNode { export function createStationMesh(station: StationSnapshot): SceneNode {
@@ -201,32 +224,160 @@ export function createShipMesh(ship: ShipSnapshot, size: number, length: number,
return createSceneNode(mesh); return createSceneNode(mesh);
} }
export function createBackdropStars(): THREE.Points { function createStarGlowTexture(documentRef: Document): THREE.CanvasTexture {
const starCount = 1800; const canvas = documentRef.createElement("canvas");
const radius = 36000; canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to create star glow texture");
}
const gradient = context.createRadialGradient(64, 64, 0, 64, 64, 64);
gradient.addColorStop(0, "rgba(255,255,255,1)");
gradient.addColorStop(0.14, "rgba(255,255,255,0.95)");
gradient.addColorStop(0.35, "rgba(255,255,255,0.42)");
gradient.addColorStop(0.68, "rgba(180,205,255,0.1)");
gradient.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = gradient;
context.fillRect(0, 0, 128, 128);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
function createStarSparkleTexture(documentRef: Document): THREE.CanvasTexture {
const canvas = documentRef.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to create star sparkle texture");
}
context.clearRect(0, 0, 128, 128);
context.translate(64, 64);
context.lineCap = "round";
const bloom = context.createRadialGradient(0, 0, 0, 0, 0, 48);
bloom.addColorStop(0, "rgba(255,255,255,0.95)");
bloom.addColorStop(0.3, "rgba(255,255,255,0.24)");
bloom.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = bloom;
context.beginPath();
context.arc(0, 0, 48, 0, Math.PI * 2);
context.fill();
context.strokeStyle = "rgba(255,255,255,0.75)";
context.lineWidth = 4;
context.beginPath();
context.moveTo(-38, 0);
context.lineTo(38, 0);
context.moveTo(0, -38);
context.lineTo(0, 38);
context.stroke();
context.rotate(Math.PI / 4);
context.strokeStyle = "rgba(255,255,255,0.35)";
context.lineWidth = 2;
context.beginPath();
context.moveTo(-28, 0);
context.lineTo(28, 0);
context.moveTo(0, -28);
context.lineTo(0, 28);
context.stroke();
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
function createMilkyWayTexture(documentRef: Document): THREE.CanvasTexture {
const canvas = documentRef.createElement("canvas");
canvas.width = 1024;
canvas.height = 256;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Unable to create milky way texture");
}
const background = context.createLinearGradient(0, 0, 1024, 0);
background.addColorStop(0, "rgba(0,0,0,0)");
background.addColorStop(0.1, "rgba(150,110,255,0.08)");
background.addColorStop(0.32, "rgba(120,210,255,0.14)");
background.addColorStop(0.5, "rgba(255,240,220,0.28)");
background.addColorStop(0.68, "rgba(255,165,210,0.16)");
background.addColorStop(0.88, "rgba(115,155,255,0.08)");
background.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = background;
context.fillRect(0, 0, 1024, 256);
for (let index = 0; index < 220; index += 1) {
const x = THREE.MathUtils.randFloat(0, 1024);
const y = 128 + THREE.MathUtils.randFloatSpread(78);
const radiusX = THREE.MathUtils.randFloat(40, 180);
const radiusY = THREE.MathUtils.randFloat(8, 28);
const alpha = THREE.MathUtils.randFloat(0.025, 0.09);
const hue = THREE.MathUtils.randFloat(0.52, 0.76);
const color = new THREE.Color().setHSL(hue, THREE.MathUtils.randFloat(0.25, 0.6), THREE.MathUtils.randFloat(0.72, 0.9));
const puff = context.createRadialGradient(x, y, 0, x, y, radiusX);
puff.addColorStop(0, `rgba(${Math.round(color.r * 255)},${Math.round(color.g * 255)},${Math.round(color.b * 255)},${alpha})`);
puff.addColorStop(0.55, `rgba(${Math.round(color.r * 255)},${Math.round(color.g * 255)},${Math.round(color.b * 255)},${alpha * 0.45})`);
puff.addColorStop(1, "rgba(0,0,0,0)");
context.save();
context.translate(x, y);
context.scale(1, radiusY / radiusX);
context.fillStyle = puff;
context.beginPath();
context.arc(0, 0, radiusX, 0, Math.PI * 2);
context.fill();
context.restore();
}
for (let index = 0; index < 540; index += 1) {
const x = THREE.MathUtils.randFloat(0, 1024);
const y = 128 + THREE.MathUtils.randFloatSpread(54);
const alpha = THREE.MathUtils.randFloat(0.12, 0.65);
const size = THREE.MathUtils.randFloat(0.8, 2.4);
context.fillStyle = `rgba(255,255,255,${alpha})`;
context.fillRect(x, y, size, size);
}
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
function sampleBackdropStarColor(): THREE.Color {
const roll = Math.random();
if (roll < 0.1) {
return new THREE.Color().setHSL(0.08, THREE.MathUtils.randFloat(0.65, 0.9), THREE.MathUtils.randFloat(0.78, 0.9));
}
if (roll < 0.28) {
return new THREE.Color().setHSL(0.58, THREE.MathUtils.randFloat(0.28, 0.55), THREE.MathUtils.randFloat(0.78, 0.9));
}
if (roll < 0.92) {
return new THREE.Color().setHSL(0.61, THREE.MathUtils.randFloat(0.08, 0.3), THREE.MathUtils.randFloat(0.84, 0.97));
}
return new THREE.Color().setHSL(0.76, THREE.MathUtils.randFloat(0.25, 0.48), THREE.MathUtils.randFloat(0.78, 0.88));
}
function createStarPointLayer(radius: number, starCount: number, size: number, opacity: number): THREE.Points {
const positions = new Float32Array(starCount * 3); const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3); const colors = new Float32Array(starCount * 3);
const color = new THREE.Color();
for (let index = 0; index < starCount; index += 1) { for (let index = 0; index < starCount; index += 1) {
const direction = new THREE.Vector3( const direction = new THREE.Vector3(
THREE.MathUtils.randFloatSpread(2), THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2), THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2), THREE.MathUtils.randFloatSpread(2),
).normalize().multiplyScalar(radius * THREE.MathUtils.randFloat(0.82, 1)); ).normalize().multiplyScalar(radius * THREE.MathUtils.randFloat(0.83, 1));
const color = sampleBackdropStarColor().multiplyScalar(THREE.MathUtils.randFloat(0.55, 1.2));
positions[index * 3] = direction.x; positions[index * 3] = direction.x;
positions[index * 3 + 1] = direction.y; positions[index * 3 + 1] = direction.y;
positions[index * 3 + 2] = direction.z; positions[index * 3 + 2] = direction.z;
const tint = THREE.MathUtils.randFloat(0, 1);
color.setRGB(
THREE.MathUtils.lerp(0.68, 1, tint),
THREE.MathUtils.lerp(0.76, 0.94, tint),
THREE.MathUtils.lerp(0.9, 1, tint),
);
if (Math.random() < 0.08) {
color.lerp(new THREE.Color(0xffd6a0), 0.45);
}
colors[index * 3] = color.r; colors[index * 3] = color.r;
colors[index * 3 + 1] = color.g; colors[index * 3 + 1] = color.g;
colors[index * 3 + 2] = color.b; colors[index * 3 + 2] = color.b;
@@ -239,77 +390,244 @@ export function createBackdropStars(): THREE.Points {
return new THREE.Points( return new THREE.Points(
geometry, geometry,
new THREE.PointsMaterial({ new THREE.PointsMaterial({
size: 2.2, size,
sizeAttenuation: false, sizeAttenuation: false,
vertexColors: true, vertexColors: true,
transparent: true, transparent: true,
opacity: 0.9, opacity,
depthWrite: false, depthWrite: false,
blending: THREE.AdditiveBlending, blending: THREE.AdditiveBlending,
fog: false,
}), }),
); );
} }
export function createBackdropStars(documentRef: Document): THREE.Group {
const radius = 36000;
const root = new THREE.Group();
root.add(
createStarPointLayer(radius, 2800, 1.15, 0.5),
createStarPointLayer(radius, 900, 1.9, 0.85),
createStarPointLayer(radius, 240, 3.1, 0.95),
);
const glowTexture = createStarGlowTexture(documentRef);
const sparkleTexture = createStarSparkleTexture(documentRef);
for (let index = 0; index < 72; index += 1) {
const direction = new THREE.Vector3(
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2),
THREE.MathUtils.randFloatSpread(2),
).normalize().multiplyScalar(radius * THREE.MathUtils.randFloat(0.84, 0.98));
const color = sampleBackdropStarColor().multiplyScalar(THREE.MathUtils.randFloat(0.9, 1.45));
const glow = new THREE.Sprite(new THREE.SpriteMaterial({
map: glowTexture,
color,
transparent: true,
opacity: THREE.MathUtils.randFloat(0.5, 0.95),
depthWrite: false,
blending: THREE.AdditiveBlending,
fog: false,
}));
const sparkle = new THREE.Sprite(new THREE.SpriteMaterial({
map: sparkleTexture,
color: color.clone().lerp(new THREE.Color(0xffffff), 0.35),
transparent: true,
opacity: THREE.MathUtils.randFloat(0.2, 0.55),
depthWrite: false,
blending: THREE.AdditiveBlending,
fog: false,
}));
const glowScale = THREE.MathUtils.randFloat(120, 260);
glow.position.copy(direction);
glow.scale.set(glowScale, glowScale, 1);
sparkle.position.copy(direction);
sparkle.material.rotation = THREE.MathUtils.randFloat(0, Math.PI);
sparkle.scale.set(glowScale * THREE.MathUtils.randFloat(0.9, 1.4), glowScale * THREE.MathUtils.randFloat(0.9, 1.4), 1);
root.add(glow, sparkle);
}
return root;
}
export function createPlanetTexture(color: string, seed: number, documentRef: Document): THREE.CanvasTexture {
const W = 256, H = 128;
const canvas = documentRef.createElement("canvas");
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Unable to create planet texture");
const imageData = ctx.createImageData(W, H);
const base = new THREE.Color(color);
function hash(x: number, y: number): number {
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.3) * 43758.5453;
return n - Math.floor(n);
}
function smoothNoise(x: number, y: number): number {
const ix = Math.floor(x), iy = Math.floor(y);
const fx = x - ix, fy = y - iy;
const ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
const a = hash(ix, iy), b = hash(ix + 1, iy);
const c = hash(ix, iy + 1), d = hash(ix + 1, iy + 1);
return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;
}
function fbm(x: number, y: number): number {
let v = 0, amp = 0.5, freq = 1;
for (let i = 0; i < 5; i++) { v += smoothNoise(x * freq, y * freq) * amp; amp *= 0.5; freq *= 2; }
return v;
}
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const nx = (x / W) * 5, ny = (y / H) * 3;
const turb = fbm(nx + 0.1, ny + 0.1) - 0.5;
const band = Math.sin((y / H * 10 + turb * 3) * Math.PI);
const light = 0.62 + band * 0.38;
const idx = (y * W + x) * 4;
imageData.data[idx] = Math.min(255, base.r * 255 * light);
imageData.data[idx + 1] = Math.min(255, base.g * 255 * light);
imageData.data[idx + 2] = Math.min(255, base.b * 255 * light);
imageData.data[idx + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.needsUpdate = true;
return texture;
}
export function createNebulaTexture(documentRef: Document): THREE.CanvasTexture { export function createNebulaTexture(documentRef: Document): THREE.CanvasTexture {
const canvas = documentRef.createElement("canvas"); const canvas = documentRef.createElement("canvas");
canvas.width = 256; canvas.width = 512;
canvas.height = 256; canvas.height = 512;
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
if (!context) { if (!context) {
throw new Error("Unable to create nebula texture"); throw new Error("Unable to create nebula texture");
} }
const gradient = context.createRadialGradient(128, 128, 18, 128, 128, 118); const palettes = [
gradient.addColorStop(0, "rgba(255,255,255,0.95)"); ["rgba(80,220,255,0.24)", "rgba(120,110,255,0.18)", "rgba(255,255,255,0.14)"],
gradient.addColorStop(0.2, "rgba(255,255,255,0.48)"); ["rgba(255,130,205,0.24)", "rgba(110,170,255,0.16)", "rgba(255,240,255,0.12)"],
gradient.addColorStop(0.55, "rgba(140,180,255,0.14)"); ["rgba(120,255,205,0.2)", "rgba(100,160,255,0.18)", "rgba(255,255,255,0.1)"],
gradient.addColorStop(1, "rgba(0,0,0,0)"); ];
context.fillStyle = gradient;
context.fillRect(0, 0, 256, 256);
for (let index = 0; index < 10; index += 1) { context.clearRect(0, 0, 512, 512);
const x = THREE.MathUtils.randFloat(30, 226);
const y = THREE.MathUtils.randFloat(30, 226); for (let layer = 0; layer < palettes.length; layer += 1) {
const radius = THREE.MathUtils.randFloat(24, 72); for (let index = 0; index < 18; index += 1) {
const puff = context.createRadialGradient(x, y, 0, x, y, radius); const x = THREE.MathUtils.randFloat(40, 472);
puff.addColorStop(0, "rgba(255,255,255,0.16)"); const y = THREE.MathUtils.randFloat(40, 472);
puff.addColorStop(0.45, "rgba(255,255,255,0.08)"); const radius = THREE.MathUtils.randFloat(55, 180);
puff.addColorStop(1, "rgba(0,0,0,0)"); const [core, mid, edge] = palettes[layer];
context.fillStyle = puff; const puff = context.createRadialGradient(x, y, 0, x, y, radius);
puff.addColorStop(0, core);
puff.addColorStop(0.4, mid);
puff.addColorStop(0.78, edge);
puff.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = puff;
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2);
context.fill();
}
}
for (let index = 0; index < 36; index += 1) {
const x = THREE.MathUtils.randFloat(50, 462);
const y = THREE.MathUtils.randFloat(50, 462);
const radius = THREE.MathUtils.randFloat(18, 60);
const glow = context.createRadialGradient(x, y, 0, x, y, radius);
glow.addColorStop(0, "rgba(255,255,255,0.12)");
glow.addColorStop(0.4, "rgba(255,255,255,0.05)");
glow.addColorStop(1, "rgba(0,0,0,0)");
context.fillStyle = glow;
context.beginPath(); context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2); context.arc(x, y, radius, 0, Math.PI * 2);
context.fill(); context.fill();
} }
// Feather the entire texture toward the borders so large sprites do not show a card-like cutoff.
const edgeFade = context.createRadialGradient(256, 256, 86, 256, 256, 256);
edgeFade.addColorStop(0, "rgba(255,255,255,1)");
edgeFade.addColorStop(0.58, "rgba(255,255,255,0.96)");
edgeFade.addColorStop(0.82, "rgba(255,255,255,0.42)");
edgeFade.addColorStop(1, "rgba(255,255,255,0)");
context.globalCompositeOperation = "destination-in";
context.fillStyle = edgeFade;
context.fillRect(0, 0, 512, 512);
context.globalCompositeOperation = "source-over";
const texture = new THREE.CanvasTexture(canvas); const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true; texture.needsUpdate = true;
return texture; return texture;
} }
export function createNebulaClouds(texture: THREE.Texture): THREE.Sprite[] { export function createNebulaClouds(texture: THREE.Texture): THREE.Sprite[] {
const directions = [ const seeds = [
new THREE.Vector3(0.74, 0.34, -0.58), { direction: new THREE.Vector3(0.76, 0.28, -0.58), color: "#5bd4ff", scale: 24000, opacity: 0.22, rotation: 0.18 },
new THREE.Vector3(-0.62, 0.18, -0.77), { direction: new THREE.Vector3(0.7, 0.34, -0.54), color: "#93b3ff", scale: 18000, opacity: 0.16, rotation: -0.22 },
new THREE.Vector3(0.22, -0.44, -0.87), { direction: new THREE.Vector3(-0.58, 0.24, -0.78), color: "#ff8cc6", scale: 22000, opacity: 0.2, rotation: 0.34 },
new THREE.Vector3(-0.38, 0.56, 0.73), { direction: new THREE.Vector3(-0.48, 0.14, -0.86), color: "#8a8dff", scale: 16000, opacity: 0.14, rotation: -0.4 },
{ direction: new THREE.Vector3(0.24, -0.46, -0.85), color: "#79ffd6", scale: 20000, opacity: 0.17, rotation: 0.52 },
{ direction: new THREE.Vector3(-0.34, 0.58, 0.74), color: "#79b7ff", scale: 26000, opacity: 0.16, rotation: -0.12 },
]; ];
return directions.map((direction, index) => { return seeds.map((seed, index) => {
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: texture, map: texture,
transparent: true, transparent: true,
opacity: 0.14, opacity: seed.opacity,
depthWrite: false, depthWrite: false,
color: ["#6dc7ff", "#ff9ec8", "#8e7dff", "#7ce0c3"][index] ?? "#6dc7ff", color: seed.color,
blending: THREE.AdditiveBlending, blending: THREE.AdditiveBlending,
fog: false,
})); }));
sprite.position.copy(direction.normalize().multiplyScalar(25000 + index * 2600)); sprite.position.copy(seed.direction.normalize().multiplyScalar(23000 + index * 1800));
const scale = 15000 + index * 2400; sprite.material.rotation = seed.rotation;
sprite.scale.set(scale, scale * 0.62, 1); sprite.scale.set(seed.scale, seed.scale * THREE.MathUtils.randFloat(0.52, 0.78), 1);
return sprite; return sprite;
}); });
} }
export function createMilkyWayBand(documentRef: Document): THREE.Group {
const radius = 33800;
const texture = createMilkyWayTexture(documentRef);
const root = new THREE.Group();
const planeNormal = new THREE.Vector3(0.24, 0.92, -0.3).normalize();
const tangent = new THREE.Vector3().crossVectors(planeNormal, new THREE.Vector3(0, 0, 1));
if (tangent.lengthSq() < 1e-6) {
tangent.set(1, 0, 0);
}
tangent.normalize();
const bitangent = new THREE.Vector3().crossVectors(planeNormal, tangent).normalize();
for (let index = 0; index < 8; index += 1) {
const angle = (index / 8) * Math.PI * 2;
const direction = tangent.clone().multiplyScalar(Math.cos(angle)).add(bitangent.clone().multiplyScalar(Math.sin(angle)));
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: index % 2 === 0 ? 0.22 : 0.15,
depthWrite: false,
blending: THREE.AdditiveBlending,
color: index % 3 === 0 ? "#ffd3f1" : index % 3 === 1 ? "#c8d8ff" : "#ffffff",
fog: false,
}));
sprite.position.copy(direction.multiplyScalar(radius));
sprite.scale.set(16500, 4300 + (index % 3) * 800, 1);
sprite.material.rotation = angle + Math.PI / 2;
root.add(sprite);
}
return root;
}
export function createTacticalIcon(documentRef: Document, color: string, size: number): SceneNode { export function createTacticalIcon(documentRef: Document, color: string, size: number): SceneNode {
const canvas = documentRef.createElement("canvas"); const canvas = documentRef.createElement("canvas");
canvas.width = 64; canvas.width = 64;
@@ -325,12 +643,6 @@ export function createTacticalIcon(documentRef: Document, color: string, size: n
context.beginPath(); context.beginPath();
context.arc(32, 32, 18, 0, Math.PI * 2); context.arc(32, 32, 18, 0, Math.PI * 2);
context.stroke(); context.stroke();
context.beginPath();
context.moveTo(32, 8);
context.lineTo(32, 56);
context.moveTo(8, 32);
context.lineTo(56, 32);
context.stroke();
const texture = new THREE.CanvasTexture(canvas); const texture = new THREE.CanvasTexture(canvas);
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ const sprite = new THREE.Sprite(new THREE.SpriteMaterial({

View File

@@ -10,7 +10,6 @@ import type {
ClaimVisual, ClaimVisual,
ConstructionSiteVisual, ConstructionSiteVisual,
NodeVisual, NodeVisual,
OrbitLineVisual,
PlanetVisual, PlanetVisual,
Selectable, Selectable,
ShipVisual, ShipVisual,
@@ -48,6 +47,7 @@ import {
createNodeMesh, createNodeMesh,
createPlanetOrbit, createPlanetOrbit,
createPlanetRing, createPlanetRing,
createPlanetTexture,
createShellReticle, createShellReticle,
createShipMesh, createShipMesh,
createCelestialMesh, createCelestialMesh,
@@ -86,7 +86,6 @@ interface SceneSyncContext {
systemSelectableTargets: Map<THREE.Object3D, Selectable>; systemSelectableTargets: Map<THREE.Object3D, Selectable>;
systemVisuals: Map<string, SystemVisual>; systemVisuals: Map<string, SystemVisual>;
planetVisuals: PlanetVisual[]; planetVisuals: PlanetVisual[];
orbitLines: OrbitLineVisual[];
celestialVisuals: Map<string, CelestialVisual>; celestialVisuals: Map<string, CelestialVisual>;
nodeVisuals: Map<string, NodeVisual>; nodeVisuals: Map<string, NodeVisual>;
stationVisuals: Map<string, StructureVisual>; stationVisuals: Map<string, StructureVisual>;
@@ -121,7 +120,6 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
context.galaxySelectableTargets.clear(); context.galaxySelectableTargets.clear();
context.systemSelectableTargets.clear(); context.systemSelectableTargets.clear();
context.planetVisuals.length = 0; context.planetVisuals.length = 0;
context.orbitLines.length = 0;
context.systemVisuals.clear(); context.systemVisuals.clear();
for (const system of systems) { for (const system of systems) {
@@ -129,7 +127,7 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
const galaxyRoot = createSceneNode(new THREE.Group()); const galaxyRoot = createSceneNode(new THREE.Group());
galaxyRoot.setPosition(toDisplayGalaxyVector(system.galaxyPosition)); galaxyRoot.setPosition(toDisplayGalaxyVector(system.galaxyPosition));
const systemIcon = createStarDot(context.documentRef, system.starColor); const systemIcon = createStarDot(context.documentRef, system.stars[0]?.color ?? "#ffffff");
const shellReticle = createShellReticle(context.documentRef, "#ff3b30", 400); const shellReticle = createShellReticle(context.documentRef, "#ff3b30", 400);
galaxyRoot.add(systemIcon, shellReticle); galaxyRoot.add(systemIcon, shellReticle);
@@ -150,50 +148,45 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
); );
for (const [planetIndex, planet] of system.planets.entries()) { for (const [planetIndex, planet] of system.planets.entries()) {
const orbit = createPlanetOrbit(planet);
const renderedPlanetRadius = celestialRenderRadius(planet.size, 0.00012, 0.03, 0.62); const renderedPlanetRadius = celestialRenderRadius(planet.size, 0.00012, 0.03, 0.62);
const planetTexture = createPlanetTexture(planet.color, planetIndex * 17 + system.id.length * 31, context.documentRef);
const planetMesh = createSceneNode(new THREE.Mesh( const planetMesh = createSceneNode(new THREE.Mesh(
new THREE.SphereGeometry(renderedPlanetRadius, 18, 18), new THREE.SphereGeometry(renderedPlanetRadius, 24, 24),
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: planet.color, map: planetTexture,
roughness: 0.92, roughness: 0.88,
metalness: 0.08, metalness: 0.04,
emissive: new THREE.Color(planet.color).multiplyScalar(0.04),
}), }),
)); ));
const initialPos = toSystemPos(computePlanetLocalPosition(planet, worldTimeSeconds)); const initialPos = toSystemPos(computePlanetLocalPosition(planet, worldTimeSeconds));
planetMesh.setPosition(initialPos); planetMesh.setPosition(initialPos);
const planetIcon = createTacticalIcon(context.documentRef, planet.color, Math.max(24, renderedPlanetRadius * 2)); const iconBaseScale = Math.max(120, renderedPlanetRadius * 10);
const planetHsl = { h: 0, s: 0, l: 0 };
new THREE.Color(planet.color).getHSL(planetHsl);
const planetIconColor = new THREE.Color().setHSL(planetHsl.h, Math.max(planetHsl.s, 0.5), 0.72).getStyle();
const planetIcon = createTacticalIcon(context.documentRef, planetIconColor, iconBaseScale);
planetIcon.setPosition(initialPos); planetIcon.setPosition(initialPos);
planetIcon.setVisible(true);
const orbit = createPlanetOrbit(planet);
const ring = planet.hasRing ? createPlanetRing(planet) : undefined; const ring = planet.hasRing ? createPlanetRing(planet) : undefined;
if (ring) { if (ring) {
ring.setPosition(initialPos); ring.setPosition(initialPos);
} }
const moons = createMoonVisuals(planet, context.worldSeed); const moons = createMoonVisuals(planet, context.documentRef);
detailGroup.add(orbit, planetMesh, planetIcon); detailGroup.add(orbit, planetMesh, planetIcon);
if (ring) { if (ring) {
detailGroup.add(ring); detailGroup.add(ring);
} }
for (const moon of moons) { for (const [moonIdx, moon] of moons.entries()) {
moon.systemId = system.id; moon.systemId = system.id;
moon.planetIndex = planetIndex; moon.planetIndex = planetIndex;
moon.orbit.setPosition(initialPos);
moon.mesh.setPosition(initialPos); moon.mesh.setPosition(initialPos);
detailGroup.add(moon.orbit, moon.mesh); moon.icon.setPosition(initialPos);
context.orbitLines.push({ detailGroup.add(moon.mesh, moon.icon, moon.orbit);
line: moon.orbit, registerSelectableTarget(context.systemSelectableTargets, moon.mesh, { kind: "moon", systemId: system.id, planetIndex, moonIndex: moonIdx });
systemId: system.id, registerSelectableTarget(context.systemSelectableTargets, moon.icon, { kind: "moon", systemId: system.id, planetIndex, moonIndex: moonIdx });
kind: "moon",
planetIndex,
});
} }
context.orbitLines.push({ context.planetVisuals.push({ systemId: system.id, planet, orbit, mesh: planetMesh, icon: planetIcon, iconBaseScale, ring, moons });
line: orbit,
systemId: system.id,
kind: "planet",
planetIndex,
});
context.planetVisuals.push({ systemId: system.id, planet, orbit, mesh: planetMesh, icon: planetIcon, ring, moons });
registerSelectableTarget(context.systemSelectableTargets, planetMesh, { kind: "planet", systemId: system.id, planetIndex }); registerSelectableTarget(context.systemSelectableTargets, planetMesh, { kind: "planet", systemId: system.id, planetIndex });
registerSelectableTarget(context.systemSelectableTargets, planetIcon, { kind: "planet", systemId: system.id, planetIndex }); registerSelectableTarget(context.systemSelectableTargets, planetIcon, { kind: "planet", systemId: system.id, planetIndex });
} }
@@ -225,7 +218,8 @@ export function syncCelestials(context: SceneSyncContext, celestials: CelestialS
} }
const mesh = createCelestialMesh(celestial, context.celestialColor); const mesh = createCelestialMesh(celestial, context.celestialColor);
const icon = createTacticalIcon(context.documentRef, context.celestialColor(celestial.kind), 18); const celestialIconBaseScale = 90;
const icon = createTacticalIcon(context.documentRef, context.celestialColor(celestial.kind), celestialIconBaseScale);
const orbitalAnchor = toSystemPos(toThreeVector(celestial.orbitalAnchor)); const orbitalAnchor = toSystemPos(toThreeVector(celestial.orbitalAnchor));
mesh.setPosition(orbitalAnchor); mesh.setPosition(orbitalAnchor);
icon.setPosition(orbitalAnchor); icon.setPosition(orbitalAnchor);
@@ -237,6 +231,7 @@ export function syncCelestials(context: SceneSyncContext, celestials: CelestialS
systemId: celestial.systemId, systemId: celestial.systemId,
mesh, mesh,
icon, icon,
iconBaseScale: celestialIconBaseScale,
kind: celestial.kind, kind: celestial.kind,
orbitalAnchor, orbitalAnchor,
}); });
@@ -252,7 +247,7 @@ export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot
for (const node of nodes) { for (const node of nodes) {
const mesh = createNodeMesh(node); const mesh = createNodeMesh(node);
const icon = createTacticalIcon(context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 20); const icon = createTacticalIcon(context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 100);
const localPosition = toThreeVector(node.localPosition); const localPosition = toThreeVector(node.localPosition);
const displayPos = toSystemPos(localPosition); const displayPos = toSystemPos(localPosition);
mesh.setPosition(displayPos); mesh.setPosition(displayPos);
@@ -285,7 +280,7 @@ export function syncStations(context: SceneSyncContext, stations: StationSnapsho
for (const station of stations) { for (const station of stations) {
const mesh = createStationMesh(station); const mesh = createStationMesh(station);
const icon = createTacticalIcon(context.documentRef, station.color, 26); const icon = createTacticalIcon(context.documentRef, station.color, 130);
const localPosition = toThreeVector(station.localPosition); const localPosition = toThreeVector(station.localPosition);
const displayPos = toSystemPos(localPosition); const displayPos = toSystemPos(localPosition);
mesh.setPosition(displayPos); mesh.setPosition(displayPos);
@@ -320,7 +315,7 @@ export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[], a
const localPosition = context.resolvePointPosition(claim.systemId, claim.celestialId); const localPosition = context.resolvePointPosition(claim.systemId, claim.celestialId);
const displayPos = toSystemPos(localPosition); const displayPos = toSystemPos(localPosition);
const mesh = createClaimMesh(claim); const mesh = createClaimMesh(claim);
const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 18); const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 90);
mesh.setPosition(displayPos); mesh.setPosition(displayPos);
icon.setPosition(displayPos); icon.setPosition(displayPos);
const isActive = claim.systemId === activeSystemId; const isActive = claim.systemId === activeSystemId;
@@ -348,7 +343,7 @@ export function syncConstructionSites(context: SceneSyncContext, sites: Construc
const localPosition = context.resolvePointPosition(site.systemId, site.celestialId); const localPosition = context.resolvePointPosition(site.systemId, site.celestialId);
const displayPos = toSystemPos(localPosition); const displayPos = toSystemPos(localPosition);
const mesh = createConstructionSiteMesh(site); const mesh = createConstructionSiteMesh(site);
const icon = createTacticalIcon(context.documentRef, "#9df29c", 18); const icon = createTacticalIcon(context.documentRef, "#9df29c", 90);
mesh.setPosition(displayPos); mesh.setPosition(displayPos);
icon.setPosition(displayPos); icon.setPosition(displayPos);
const isActive = site.systemId === activeSystemId; const isActive = site.systemId === activeSystemId;
@@ -375,7 +370,7 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
for (const ship of ships) { for (const ship of ships) {
const mesh = createShipMesh(ship, context.shipSize(ship), context.shipLength(ship), context.shipPresentationColor(ship)); const mesh = createShipMesh(ship, context.shipSize(ship), context.shipLength(ship), context.shipPresentationColor(ship));
const shipColor = context.shipPresentationColor(ship); const shipColor = context.shipPresentationColor(ship);
const icon = createTacticalIcon(context.documentRef, shipColor, 18); const icon = createTacticalIcon(context.documentRef, shipColor, 90);
const localPosition = toThreeVector(ship.localPosition); const localPosition = toThreeVector(ship.localPosition);
const displayPos = toSystemPos(localPosition); const displayPos = toSystemPos(localPosition);
mesh.setPosition(displayPos); mesh.setPosition(displayPos);

View File

@@ -33,6 +33,10 @@ export function describeSelectable(world: WorldState | undefined, item: Selectab
if (item.kind === "planet") { if (item.kind === "planet") {
return world.systems.get(item.systemId)?.planets[item.planetIndex]?.label ?? `${item.systemId}:${item.planetIndex}`; return world.systems.get(item.systemId)?.planets[item.planetIndex]?.label ?? `${item.systemId}:${item.planetIndex}`;
} }
if (item.kind === "moon") {
const planet = world.systems.get(item.systemId)?.planets[item.planetIndex];
return planet?.moons[item.moonIndex]?.label ?? `moon ${item.moonIndex + 1}`;
}
return world.systems.get(item.id)?.label ?? item.id; return world.systems.get(item.id)?.label ?? item.id;
} }
@@ -54,7 +58,7 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
if (!system) { if (!system) {
return item.id; return item.id;
} }
const starLabel = system.starCount > 1 ? `${system.starCount}× ${system.starKind}` : system.starKind; const starLabel = system.stars.length > 1 ? `${system.stars.length}× ${system.stars[0]?.kind}` : (system.stars[0]?.kind ?? "unknown");
const planetCount = system.planets.length; const planetCount = system.planets.length;
const shipCount = [...world.ships.values()].filter((s) => s.systemId === item.id).length; const shipCount = [...world.ships.values()].filter((s) => s.systemId === item.id).length;
const stationCount = [...world.stations.values()].filter((s) => s.systemId === item.id).length; const stationCount = [...world.stations.values()].filter((s) => s.systemId === item.id).length;
@@ -81,6 +85,16 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
return planet ? `${system?.label ?? item.systemId} / ${planet.label}` : `${item.systemId} / planet ${item.planetIndex + 1}`; return planet ? `${system?.label ?? item.systemId} / ${planet.label}` : `${item.systemId} / planet ${item.planetIndex + 1}`;
} }
if (item.kind === "moon") {
const system = world.systems.get(item.systemId);
const planet = system?.planets[item.planetIndex];
const moon = planet?.moons[item.moonIndex];
if (moon) {
return `${system?.label ?? item.systemId} / ${planet?.label ?? `planet ${item.planetIndex + 1}`} / ${moon.label}`;
}
return `${item.systemId} / planet ${item.planetIndex + 1} / moon ${item.moonIndex + 1}`;
}
if (item.kind === "node") { if (item.kind === "node") {
const node = world.nodes.get(item.id); const node = world.nodes.get(item.id);
if (!node) { if (!node) {
@@ -168,6 +182,9 @@ export function resolveSelectableSystemId(world: WorldState | undefined, selecti
if (selection.kind === "planet") { if (selection.kind === "planet") {
return selection.systemId; return selection.systemId;
} }
if (selection.kind === "moon") {
return selection.systemId;
}
return selection.id; return selection.id;
} }
@@ -271,7 +288,7 @@ export function renderSystemDetails(
} }
} }
for (const planet of system.planets) { for (const planet of system.planets) {
moonCount += planet.moonCount; moonCount += planet.moons.length;
} }
const followText = activeContext && cameraMode === "follow" && cameraTargetShipId const followText = activeContext && cameraMode === "follow" && cameraTargetShipId
@@ -280,7 +297,7 @@ export function renderSystemDetails(
return ` return `
<p>${system.id}${activeContext ? " · active system" : ""}</p> <p>${system.id}${activeContext ? " · active system" : ""}</p>
<p>${system.starKind} · ${system.starCount} star${system.starCount > 1 ? "s" : ""}</p> <p>${system.stars[0]?.kind ?? "unknown"} · ${system.stars.length} star${system.stars.length > 1 ? "s" : ""}</p>
<p>Planets ${system.planets.length}<br>Moons ${moonCount}<br>Ships ${shipCount}<br>Stations ${stationCount}</p> <p>Planets ${system.planets.length}<br>Moons ${moonCount}<br>Ships ${shipCount}<br>Stations ${stationCount}</p>
<p>Celestials ${celestialCount}<br>Resource nodes ${nodeCount}</p> <p>Celestials ${celestialCount}<br>Resource nodes ${nodeCount}</p>
<p>Claims ${claimCount}<br>Construction sites ${constructionCount}</p> <p>Claims ${claimCount}<br>Construction sites ${constructionCount}</p>

View File

@@ -1,5 +1,14 @@
import * as THREE from "three"; import * as THREE from "three";
import type { Selectable } from "./viewerTypes"; import type {
CelestialVisual,
ClaimVisual,
ConstructionSiteVisual,
NodeVisual,
PlanetVisual,
Selectable,
ShipVisual,
StructureVisual,
} from "./viewerTypes";
/** /**
* System rendering layer. * System rendering layer.
@@ -9,7 +18,7 @@ import type { Selectable } from "./viewerTypes";
*/ */
export class SystemLayer { export class SystemLayer {
readonly scene = new THREE.Scene(); readonly scene = new THREE.Scene();
readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 50000); readonly camera = new THREE.PerspectiveCamera(50, 1, 0.0001, 300000);
readonly celestialGroup = new THREE.Group(); readonly celestialGroup = new THREE.Group();
readonly nodeGroup = new THREE.Group(); readonly nodeGroup = new THREE.Group();
@@ -20,6 +29,14 @@ export class SystemLayer {
readonly selectableTargets = new Map<THREE.Object3D, Selectable>(); readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
readonly planetVisuals: PlanetVisual[] = [];
readonly shipVisuals = new Map<string, ShipVisual>();
readonly celestialVisuals = new Map<string, CelestialVisual>();
readonly nodeVisuals = new Map<string, NodeVisual>();
readonly stationVisuals = new Map<string, StructureVisual>();
readonly claimVisuals = new Map<string, ClaimVisual>();
readonly constructionSiteVisuals = new Map<string, ConstructionSiteVisual>();
constructor() { constructor() {
this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.55)); this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.55));
const keyLight = new THREE.DirectionalLight(0xdcecff, 1.3); const keyLight = new THREE.DirectionalLight(0xdcecff, 1.3);
@@ -44,4 +61,8 @@ export class SystemLayer {
this.camera.aspect = aspect; this.camera.aspect = aspect;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
} }
render(renderer: THREE.WebGLRenderer) {
renderer.render(this.scene, this.camera);
}
} }

View File

@@ -29,7 +29,8 @@ export type Selectable =
| { kind: "claim"; id: string } | { kind: "claim"; id: string }
| { kind: "construction-site"; id: string } | { kind: "construction-site"; id: string }
| { kind: "system"; id: string } | { kind: "system"; id: string }
| { kind: "planet"; systemId: string; planetIndex: number }; | { kind: "planet"; systemId: string; planetIndex: number }
| { kind: "moon"; systemId: string; planetIndex: number; moonIndex: number };
export interface ShipVisual { export interface ShipVisual {
systemId: string; systemId: string;
@@ -49,6 +50,7 @@ export interface PlanetVisual {
orbit: SceneNode; orbit: SceneNode;
mesh: SceneNode; mesh: SceneNode;
icon: SceneNode; icon: SceneNode;
iconBaseScale: number;
ring?: SceneNode; ring?: SceneNode;
moons: MoonVisual[]; moons: MoonVisual[];
} }
@@ -56,17 +58,13 @@ export interface PlanetVisual {
export interface MoonVisual { export interface MoonVisual {
systemId: string; systemId: string;
planetIndex: number; planetIndex: number;
moonIndex: number;
mesh: SceneNode; mesh: SceneNode;
icon: SceneNode;
iconBaseScale: number;
orbit: SceneNode; orbit: SceneNode;
} }
export interface OrbitLineVisual {
line: SceneNode;
systemId: string;
kind: "planet" | "moon";
planetIndex: number;
}
export type OrbitalAnchor = export type OrbitalAnchor =
| { kind: "star" } | { kind: "star" }
| { kind: "planet"; planetIndex: number } | { kind: "planet"; planetIndex: number }
@@ -89,6 +87,7 @@ export interface CelestialVisual {
systemId: string; systemId: string;
mesh: SceneNode; mesh: SceneNode;
icon: SceneNode; icon: SceneNode;
iconBaseScale: number;
kind: string; kind: string;
orbitalAnchor: THREE.Vector3; orbitalAnchor: THREE.Vector3;
} }

View File

@@ -17,9 +17,11 @@ export class UniverseLayer {
this.scene.add(this.ambienceGroup); this.scene.add(this.ambienceGroup);
} }
updateAmbience(activeCamera: THREE.Camera, delta: number) { updateAmbience(activeCamera: THREE.Camera, _delta: number) {
this.ambienceGroup.position.copy(activeCamera.position); this.ambienceGroup.position.copy(activeCamera.position);
this.ambienceGroup.rotation.y += delta * 0.005; }
this.ambienceGroup.rotation.x = Math.sin(performance.now() * 0.00003) * 0.015;
render(renderer: THREE.WebGLRenderer, camera: THREE.Camera) {
renderer.render(this.scene, camera);
} }
} }

View File

@@ -4,7 +4,6 @@ import {
DISPLAY_UNITS_PER_LIGHT_YEAR, DISPLAY_UNITS_PER_LIGHT_YEAR,
KILOMETERS_PER_AU, KILOMETERS_PER_AU,
computeMoonLocalPosition, computeMoonLocalPosition,
computeMoonSize,
computePlanetLocalPosition, computePlanetLocalPosition,
currentWorldTimeSeconds, currentWorldTimeSeconds,
resolveOrbitalAnchorPosition, resolveOrbitalAnchorPosition,
@@ -16,6 +15,7 @@ import {
resolveShipHeading, resolveShipHeading,
updateSystemStarPresentation, updateSystemStarPresentation,
getAnimatedShipLocalPosition, getAnimatedShipLocalPosition,
iconWorldScale,
} from "./viewerPresentation"; } from "./viewerPresentation";
import { rawObject } from "./viewerScenePrimitives"; import { rawObject } from "./viewerScenePrimitives";
import type { import type {
@@ -114,7 +114,15 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition)); visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
visual.icon.setPosition(rawObject(visual.mesh).position.clone()); visual.icon.setPosition(rawObject(visual.mesh).position.clone());
visual.mesh.setVisible(visual.systemId === context.activeSystemId); visual.mesh.setVisible(visual.systemId === context.activeSystemId);
visual.icon.setVisible(visual.systemId === context.activeSystemId); const iconWorldPos = visual.icon.getWorldPosition(new THREE.Vector3());
const distToIcon = context.camera.position.distanceTo(iconWorldPos);
const isNearPlanetLagrange = /-l[12]$/.test(visual.id);
const inCluster = !isNearPlanetLagrange || distToIcon < 400;
visual.icon.setVisible(visual.systemId === context.activeSystemId && inCluster);
const t = THREE.MathUtils.clamp(distToIcon / 300, 0, 1);
const rawCelestialScale = visual.iconBaseScale * t * Math.sqrt(t);
const celestialIconScale = THREE.MathUtils.clamp(rawCelestialScale, iconWorldScale(distToIcon, context.camera, 15), iconWorldScale(distToIcon, context.camera, 100));
visual.icon.setScaleScalar(celestialIconScale);
} }
for (const visual of context.stationVisuals.values()) { for (const visual of context.stationVisuals.values()) {
@@ -351,13 +359,12 @@ export function resolveOrbitalAnchor(context: WorldOrbitalContext, systemId: str
bestAnchor = { kind: "planet", planetIndex }; bestAnchor = { kind: "planet", planetIndex };
} }
const moonCount = Math.min(planet.moonCount, 12); for (const [moonIndex, moon] of planet.moons.entries()) {
for (let moonIndex = 0; moonIndex < moonCount; moonIndex += 1) {
const moonPosition = planetPosition const moonPosition = planetPosition
.clone() .clone()
.add(computeMoonLocalPosition(planet, moonIndex, nowSeconds, context.world.seed)); .add(computeMoonLocalPosition(moon, nowSeconds));
const moonDistance = localPosition.distanceTo(moonPosition); const moonDistance = localPosition.distanceTo(moonPosition);
const moonThreshold = Math.max(computeMoonSize(planet, moonIndex, context.world.seed) * 14, 80); const moonThreshold = Math.max(moon.size * 14, 80);
if (moonDistance < moonThreshold && moonDistance < bestDistance) { if (moonDistance < moonThreshold && moonDistance < bestDistance) {
bestDistance = moonDistance; bestDistance = moonDistance;
bestAnchor = { kind: "moon", planetIndex, moonIndex }; bestAnchor = { kind: "moon", planetIndex, moonIndex };
@@ -417,9 +424,15 @@ export function computeCelestialLocalPositionById(
const parentInitialPosition = toThreeVector(parentCelestial.orbitalAnchor); const parentInitialPosition = toThreeVector(parentCelestial.orbitalAnchor);
const relativeOffset = basePosition.clone().sub(parentInitialPosition); const relativeOffset = basePosition.clone().sub(parentInitialPosition);
const initialAngle = Math.atan2(parentInitialPosition.z, parentInitialPosition.x); const initialDir = parentInitialPosition.clone().normalize();
const currentAngle = Math.atan2(parentCurrentPosition.z, parentCurrentPosition.x); const currentDir = parentCurrentPosition.clone().normalize();
const rotatedOffset = relativeOffset.applyAxisAngle(new THREE.Vector3(0, 1, 0), currentAngle - initialAngle); let rotatedOffset: THREE.Vector3;
if (initialDir.lengthSq() > 0.0001 && currentDir.lengthSq() > 0.0001) {
const quaternion = new THREE.Quaternion().setFromUnitVectors(initialDir, currentDir);
rotatedOffset = relativeOffset.clone().applyQuaternion(quaternion);
} else {
rotatedOffset = relativeOffset.clone();
}
return parentCurrentPosition.clone().add(rotatedOffset); return parentCurrentPosition.clone().add(rotatedOffset);
} }
@@ -486,7 +499,7 @@ function computeStructureLocalPosition(
} }
function getOrbitalAnchorPosition(context: WorldOrbitalContext, systemId: string, anchor: OrbitalAnchor, timeSeconds: number) { function getOrbitalAnchorPosition(context: WorldOrbitalContext, systemId: string, anchor: OrbitalAnchor, timeSeconds: number) {
return resolveOrbitalAnchorPosition(context.world, systemId, anchor, timeSeconds, context.worldSeed); return resolveOrbitalAnchorPosition(context.world, systemId, anchor, timeSeconds);
} }
function resolveStructureAnimatedLocalPosition(context: WorldOrbitalContext, visual: StructureVisual, timeSeconds: number) { function resolveStructureAnimatedLocalPosition(context: WorldOrbitalContext, visual: StructureVisual, timeSeconds: number) {

136
docs/SYSTEMS.md Normal file
View File

@@ -0,0 +1,136 @@
# Systems
This document describes the `shared/data/systems.json` schema and the units used throughout.
## Overview
Each entry in `systems.json` defines a solar system in the galaxy. Systems are loaded by the backend and used to build the simulation world. The galaxy position, star properties, planet orbits, asteroid fields, and resource nodes are all authored here.
---
## Top-Level Fields
| Field | Type | Unit | Description |
|---|---|---|---|
| `id` | string | — | Unique identifier, referenced by scenario, factions, routes |
| `label` | string | — | Display name |
| `position` | `[x, y, z]` | **light-years (ly)** | Position in the galaxy map. Used directly for inter-system distances and FTL transit time. Sol is at `[18.2, 0.02, -11.8]` ly, Helios at `[0, 0, 0]`. |
| `stars` | array | — | One entry per star. Single-star systems have one element; binary systems have two. See `stars` section below. |
---
## `stars`
Each star is an explicit object. Single-star systems have one entry; binary systems have two. Binary stars orbit the barycenter using the same orbital fields as planets.
| Field | Type | Unit | Description |
|---|---|---|---|
| `kind` | string | — | Classification: `"main-sequence"`, `"blue-white"`, `"white-dwarf"`, `"brown-dwarf"`, `"neutron-star"` |
| `color` | string | — | Hex color of the star body |
| `glow` | string | — | Hex color of the star's halo/corona |
| `size` | float | **km** | Radius of the star. Sol = 696,340 km |
| `orbitRadius` | float | **km** | Distance from system barycenter. `0` for single stars or the primary in a simple system. |
| `orbitSpeed` | float | **rad/s** | Angular velocity around barycenter. `0` for static stars. |
| `orbitPhaseAtEpoch` | float | **degrees** | Starting angle. |
---
## `asteroidField`
Defines the decorative asteroid belt for the system.
| Field | Type | Unit | Description |
|---|---|---|---|
| `decorationCount` | int | — | Number of decorative asteroid meshes to scatter |
| `radiusOffset` | float | **km** | Base orbit radius of the belt from the system center |
| `radiusVariance` | float | **km** | Random spread around `radiusOffset` |
| `heightVariance` | float | **km** | Vertical (Y-axis) spread of the belt |
Sol's belt (422,000,000 km base) maps to the real asteroid belt between Mars and Jupiter. Helios and Perseus use much smaller values since their orbits are scaled differently.
---
## `resourceNodes`
Each node is a mineable location in the system.
| Field | Type | Unit | Description |
|---|---|---|---|
| `sourceKind` | string | — | Type of node. e.g. `"asteroid-belt"` |
| `angle` | float | **radians** | Angular position in the orbit around the anchor |
| `radiusOffset` | float | **km** | Orbit radius around the anchor celestial |
| `inclinationDegrees` | float | **degrees** | Orbital inclination of the node's orbit |
| `anchorPlanetIndex` | int? | — | 0-based index into `planets` array. If absent, anchored to the system center |
| `oreAmount` | float | — | Starting ore amount in the node |
| `itemId` | string | — | Item ID of the mined resource |
| `shardCount` | int | — | Number of individual shards the node splits into |
---
## `planets`
Planets are defined with real Keplerian orbital elements. All angular values use the same epoch for consistency.
| Field | Type | Unit | Description |
|---|---|---|---|
| `label` | string | — | Display name |
| `planetType` | string | — | Classification: `"terrestrial"`, `"desert"`, `"barren"`, `"gas-giant"`, `"ice-giant"` |
| `shape` | string | — | Mesh hint: `"sphere"` or `"oblate"` |
| `moons` | array | — | Explicit moon definitions. See `moons` section below. |
| `orbitRadius` | float | **AU** | Semi-major axis in Astronomical Units. Converted to km at runtime using 1 AU = 149,597,870.7 km |
| `orbitSpeed` | float | **rad/s** (orbital sim seconds) | Angular velocity. Earth = 0.11 rad/s. Derived from Kepler's third law: `0.11 / sqrt(r³)` where r is in AU |
| `orbitEccentricity` | float | — | Orbital eccentricity (0 = circular) |
| `orbitInclination` | float | **degrees** | Inclination relative to the ecliptic plane. Converted to radians at runtime |
| `orbitLongitudeOfAscendingNode` | float | **degrees** | Longitude of the ascending node (Ω). Converted to radians at runtime |
| `orbitArgumentOfPeriapsis` | float | **degrees** | Argument of periapsis (ω). Converted to radians at runtime |
| `orbitPhaseAtEpoch` | float | **degrees** | Mean anomaly at epoch (initial angular position). Converted to radians at runtime |
| `size` | float | **km** | Radius of the planet. Earth = 6,371 km, Jupiter = 69,911 km |
| `color` | string | — | Hex color |
| `tilt` | float | **radians** | Axial tilt |
| `hasRing` | bool | — | Whether to render a ring system |
### `moons`
Each moon is an explicit celestial — a docking target in the simulation, just like a planet.
| Field | Type | Unit | Description |
|---|---|---|---|
| `label` | string | — | Display name |
| `size` | float | **km** | Moon radius |
| `color` | string | — | Hex color |
| `orbitRadius` | float | **km** | Orbit radius around the parent planet |
| `orbitSpeed` | float | **rad/s** | Angular velocity. Negative = retrograde (e.g. Triton). |
| `orbitPhaseAtEpoch` | float | **degrees** | Initial angle |
| `orbitInclination` | float | **degrees** | Inclination relative to the planet's equatorial plane |
| `orbitLongitudeOfAscendingNode` | float | **degrees** | Longitude of the ascending node |
For generated systems, moons are procedurally derived from the planet's size and label (seeded hash). Explicitly authored systems (Sol) define their moons directly.
---
### Orbit Speed Formula
```
orbitSpeed = 0.11 / sqrt(orbitRadius³)
```
Where `0.11` is Earth's angular speed in orbital-sim-seconds per radian, and `orbitRadius` is in AU. This gives each planet the correct relative orbital period.
Orbital sim time runs at a configurable multiplier relative to real time (`SimulatedSecondsPerRealSecond`), so the absolute speed only matters in relation to other planets, not to wall-clock time.
---
## Unit Summary
| Concept | Unit |
|---|---|
| Galaxy positions | light-years (ly) |
| Star / planet / moon sizes | km |
| Asteroid field radii | km |
| Resource node radii | km |
| Moon orbit radius | km |
| Planet orbit radius | AU (converted to km at runtime) |
| Planet / moon / star orbit speed | rad / orbital-sim-second |
| Orbital angles (inclination, ascending node, periapsis, phase) | degrees (converted to radians at runtime) |
| Resource node angle | radians |
| Resource node inclination | degrees |

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,10 @@
{ {
"label": "Orbital Station", "label": "Orbital Station",
"startingModules": [ "startingModules": [
"dock-bay-small", "module_arg_dock_m_01_lowtech",
"power-core", "module_gen_prod_energycells_01",
"bulk-bay", "module_arg_stor_solid_m_01",
"liquid-tank" "module_arg_stor_liquid_m_01"
], ],
"systemId": "helios", "systemId": "helios",
"planetIndex": 2, "planetIndex": 2,

View File

@@ -21,35 +21,35 @@
"recipeId": "frigate-construction", "recipeId": "frigate-construction",
"facilityCategory": "station", "facilityCategory": "station",
"requiredModules": [ "requiredModules": [
"ship-factory" "module_gen_build_l_01"
], ],
"requirements": [ "requirements": [
{ {
"itemId": "hull-sections", "itemId": "hullparts",
"amount": 26 "amount": 26
}, },
{ {
"itemId": "command-bridge-module", "itemId": "advancedelectronics",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "reactor-core-module", "itemId": "antimatterconverters",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "capacitor-bank-module", "itemId": "shieldcomponents",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ion-drive-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ftl-core-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "gun-turret-module", "itemId": "turretcomponents",
"amount": 1 "amount": 1
} }
], ],
@@ -81,35 +81,35 @@
"recipeId": "destroyer-construction", "recipeId": "destroyer-construction",
"facilityCategory": "station", "facilityCategory": "station",
"requiredModules": [ "requiredModules": [
"ship-factory" "module_gen_build_l_01"
], ],
"requirements": [ "requirements": [
{ {
"itemId": "hull-sections", "itemId": "hullparts",
"amount": 44 "amount": 44
}, },
{ {
"itemId": "command-bridge-module", "itemId": "advancedelectronics",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "reactor-core-module", "itemId": "antimatterconverters",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "capacitor-bank-module", "itemId": "shieldcomponents",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ion-drive-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ftl-core-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "gun-turret-module", "itemId": "turretcomponents",
"amount": 2 "amount": 2
} }
], ],
@@ -141,35 +141,35 @@
"recipeId": "cruiser-construction", "recipeId": "cruiser-construction",
"facilityCategory": "station", "facilityCategory": "station",
"requiredModules": [ "requiredModules": [
"ship-factory" "module_gen_build_l_01"
], ],
"requirements": [ "requirements": [
{ {
"itemId": "hull-sections", "itemId": "hullparts",
"amount": 60 "amount": 60
}, },
{ {
"itemId": "command-bridge-module", "itemId": "advancedelectronics",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "reactor-core-module", "itemId": "antimatterconverters",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "capacitor-bank-module", "itemId": "shieldcomponents",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ion-drive-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ftl-core-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "gun-turret-module", "itemId": "turretcomponents",
"amount": 2 "amount": 2
} }
], ],
@@ -207,39 +207,39 @@
"recipeId": "carrier-construction", "recipeId": "carrier-construction",
"facilityCategory": "station", "facilityCategory": "station",
"requiredModules": [ "requiredModules": [
"ship-factory" "module_gen_build_l_01"
], ],
"requirements": [ "requirements": [
{ {
"itemId": "hull-sections", "itemId": "hullparts",
"amount": 120 "amount": 120
}, },
{ {
"itemId": "command-bridge-module", "itemId": "advancedelectronics",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "reactor-core-module", "itemId": "antimatterconverters",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "capacitor-bank-module", "itemId": "shieldcomponents",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ion-drive-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ftl-core-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "carrier-bay-module", "itemId": "dronecomponents",
"amount": 2 "amount": 2
}, },
{ {
"itemId": "gun-turret-module", "itemId": "turretcomponents",
"amount": 1 "amount": 1
} }
], ],
@@ -272,35 +272,35 @@
"recipeId": "hauler-construction", "recipeId": "hauler-construction",
"facilityCategory": "station", "facilityCategory": "station",
"requiredModules": [ "requiredModules": [
"ship-factory" "module_gen_build_l_01"
], ],
"requirements": [ "requirements": [
{ {
"itemId": "hull-sections", "itemId": "hullparts",
"amount": 34 "amount": 34
}, },
{ {
"itemId": "command-bridge-module", "itemId": "advancedelectronics",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "reactor-core-module", "itemId": "antimatterconverters",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "capacitor-bank-module", "itemId": "shieldcomponents",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ion-drive-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ftl-core-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "container-bay-module", "itemId": "hullparts",
"amount": 1 "amount": 1
} }
], ],
@@ -333,39 +333,39 @@
"recipeId": "constructor-construction", "recipeId": "constructor-construction",
"facilityCategory": "station", "facilityCategory": "station",
"requiredModules": [ "requiredModules": [
"ship-factory" "module_gen_build_l_01"
], ],
"requirements": [ "requirements": [
{ {
"itemId": "hull-sections", "itemId": "hullparts",
"amount": 42 "amount": 42
}, },
{ {
"itemId": "command-bridge-module", "itemId": "advancedelectronics",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "reactor-core-module", "itemId": "antimatterconverters",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "capacitor-bank-module", "itemId": "shieldcomponents",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ion-drive-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ftl-core-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "fabricator-array-module", "itemId": "advancedelectronics",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "container-bay-module", "itemId": "hullparts",
"amount": 1 "amount": 1
} }
], ],
@@ -399,39 +399,39 @@
"recipeId": "miner-construction", "recipeId": "miner-construction",
"facilityCategory": "station", "facilityCategory": "station",
"requiredModules": [ "requiredModules": [
"ship-factory" "module_gen_build_l_01"
], ],
"requirements": [ "requirements": [
{ {
"itemId": "hull-sections", "itemId": "hullparts",
"amount": 34 "amount": 34
}, },
{ {
"itemId": "command-bridge-module", "itemId": "advancedelectronics",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "reactor-core-module", "itemId": "antimatterconverters",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "capacitor-bank-module", "itemId": "shieldcomponents",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ion-drive-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "ftl-core-module", "itemId": "engineparts",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "mining-turret-module", "itemId": "turretcomponents",
"amount": 1 "amount": 1
}, },
{ {
"itemId": "bulk-bay-module", "itemId": "hullparts",
"amount": 1 "amount": 1
} }
], ],

View File

@@ -1,12 +1,359 @@
[ [
{
"id": "sol",
"label": "Sol",
"position": [
18.2,
0.02,
-11.8
],
"stars": [
{
"kind": "main-sequence",
"color": "#fff1b8",
"glow": "#ffd35a",
"size": 696340
}
],
"asteroidField": {
"decorationCount": 240,
"radiusOffset": 422000000,
"radiusVariance": 180000000,
"heightVariance": 22000000
},
"resourceNodes": [
{
"sourceKind": "asteroid-belt",
"angle": 0.2,
"radiusOffset": 126000,
"inclinationDegrees": 4,
"anchorPlanetIndex": 3,
"oreAmount": 1000,
"itemId": "ore",
"shardCount": 9
},
{
"sourceKind": "asteroid-belt",
"angle": 1.8,
"radiusOffset": 148000,
"inclinationDegrees": -6,
"anchorPlanetIndex": 3,
"oreAmount": 1000,
"itemId": "ore",
"shardCount": 9
},
{
"sourceKind": "asteroid-belt",
"angle": 3.5,
"radiusOffset": 138000,
"inclinationDegrees": 8,
"anchorPlanetIndex": 4,
"oreAmount": 1000,
"itemId": "ore",
"shardCount": 9
},
{
"sourceKind": "asteroid-belt",
"angle": 5.1,
"radiusOffset": 164000,
"inclinationDegrees": -5,
"anchorPlanetIndex": 4,
"oreAmount": 1000,
"itemId": "ore",
"shardCount": 9
}
],
"planets": [
{
"label": "Mercury",
"planetType": "barren",
"shape": "sphere",
"moons": [],
"orbitRadius": 0.3871,
"orbitSpeed": 0.4567,
"orbitEccentricity": 0.2056,
"orbitInclination": 7.0,
"orbitLongitudeOfAscendingNode": 48,
"orbitArgumentOfPeriapsis": 29,
"orbitPhaseAtEpoch": 252,
"size": 2440,
"color": "#b7a08f",
"tilt": 0.03,
"hasRing": false
},
{
"label": "Venus",
"planetType": "desert",
"shape": "sphere",
"moons": [],
"orbitRadius": 0.7233,
"orbitSpeed": 0.1788,
"orbitEccentricity": 0.0067,
"orbitInclination": 3.4,
"orbitLongitudeOfAscendingNode": 76,
"orbitArgumentOfPeriapsis": 54,
"orbitPhaseAtEpoch": 181,
"size": 6052,
"color": "#d9b38c",
"tilt": 2.64,
"hasRing": false
},
{
"label": "Earth",
"planetType": "terrestrial",
"shape": "sphere",
"moons": [
{
"label": "Moon",
"size": 1737,
"color": "#c0bdb7",
"orbitRadius": 384400,
"orbitSpeed": 2.664,
"orbitPhaseAtEpoch": 135,
"orbitInclination": 5.1,
"orbitLongitudeOfAscendingNode": 0
}
],
"orbitRadius": 1.0,
"orbitSpeed": 0.11,
"orbitEccentricity": 0.0167,
"orbitInclination": 0.0,
"orbitLongitudeOfAscendingNode": 0,
"orbitArgumentOfPeriapsis": 114,
"orbitPhaseAtEpoch": 100,
"size": 6371,
"color": "#4f84c4",
"tilt": 0.41,
"hasRing": false
},
{
"label": "Mars",
"planetType": "desert",
"shape": "sphere",
"moons": [
{
"label": "Phobos",
"size": 11,
"color": "#b0a898",
"orbitRadius": 9376,
"orbitSpeed": 19.84,
"orbitPhaseAtEpoch": 20,
"orbitInclination": 1.1,
"orbitLongitudeOfAscendingNode": 0
},
{
"label": "Deimos",
"size": 6,
"color": "#b8b0a4",
"orbitRadius": 23463,
"orbitSpeed": 7.72,
"orbitPhaseAtEpoch": 200,
"orbitInclination": 1.8,
"orbitLongitudeOfAscendingNode": 0
}
],
"orbitRadius": 1.5237,
"orbitSpeed": 0.05847,
"orbitEccentricity": 0.0934,
"orbitInclination": 1.85,
"orbitLongitudeOfAscendingNode": 49,
"orbitArgumentOfPeriapsis": 286,
"orbitPhaseAtEpoch": 54,
"size": 3390,
"color": "#c56e52",
"tilt": 0.44,
"hasRing": false
},
{
"label": "Jupiter",
"planetType": "gas-giant",
"shape": "oblate",
"moons": [
{
"label": "Io",
"size": 1821,
"color": "#e8c97a",
"orbitRadius": 421800,
"orbitSpeed": 0.8924,
"orbitPhaseAtEpoch": 0,
"orbitInclination": 0.04,
"orbitLongitudeOfAscendingNode": 0
},
{
"label": "Europa",
"size": 1561,
"color": "#c8bfa8",
"orbitRadius": 671100,
"orbitSpeed": 0.5567,
"orbitPhaseAtEpoch": 90,
"orbitInclination": 0.47,
"orbitLongitudeOfAscendingNode": 0
},
{
"label": "Ganymede",
"size": 2634,
"color": "#a89880",
"orbitRadius": 1070400,
"orbitSpeed": 0.3470,
"orbitPhaseAtEpoch": 180,
"orbitInclination": 0.18,
"orbitLongitudeOfAscendingNode": 0
},
{
"label": "Callisto",
"size": 2410,
"color": "#8c8070",
"orbitRadius": 1882700,
"orbitSpeed": 0.1836,
"orbitPhaseAtEpoch": 270,
"orbitInclination": 0.19,
"orbitLongitudeOfAscendingNode": 0
}
],
"orbitRadius": 5.203,
"orbitSpeed": 0.009274,
"orbitEccentricity": 0.0489,
"orbitInclination": 1.3,
"orbitLongitudeOfAscendingNode": 100,
"orbitArgumentOfPeriapsis": 275,
"orbitPhaseAtEpoch": 34,
"size": 69911,
"color": "#d9b06f",
"tilt": 0.05,
"hasRing": true
},
{
"label": "Saturn",
"planetType": "gas-giant",
"shape": "oblate",
"moons": [
{
"label": "Titan",
"size": 2575,
"color": "#d4a850",
"orbitRadius": 1221900,
"orbitSpeed": 0.2238,
"orbitPhaseAtEpoch": 45,
"orbitInclination": 0.33,
"orbitLongitudeOfAscendingNode": 0
},
{
"label": "Enceladus",
"size": 252,
"color": "#e8eef5",
"orbitRadius": 238000,
"orbitSpeed": 0.9328,
"orbitPhaseAtEpoch": 220,
"orbitInclination": 0.02,
"orbitLongitudeOfAscendingNode": 0
},
{
"label": "Rhea",
"size": 764,
"color": "#c8c0b0",
"orbitRadius": 527100,
"orbitSpeed": 0.3894,
"orbitPhaseAtEpoch": 130,
"orbitInclination": 0.35,
"orbitLongitudeOfAscendingNode": 0
}
],
"orbitRadius": 9.582,
"orbitSpeed": 0.003708,
"orbitEccentricity": 0.0565,
"orbitInclination": 2.49,
"orbitLongitudeOfAscendingNode": 113,
"orbitArgumentOfPeriapsis": 339,
"orbitPhaseAtEpoch": 200,
"size": 58232,
"color": "#dfc27d",
"tilt": 0.47,
"hasRing": true
},
{
"label": "Uranus",
"planetType": "ice-giant",
"shape": "oblate",
"moons": [
{
"label": "Titania",
"size": 789,
"color": "#b0b8c0",
"orbitRadius": 435910,
"orbitSpeed": 0.3560,
"orbitPhaseAtEpoch": 60,
"orbitInclination": 0.08,
"orbitLongitudeOfAscendingNode": 0
},
{
"label": "Oberon",
"size": 761,
"color": "#a0a8b0",
"orbitRadius": 583520,
"orbitSpeed": 0.2706,
"orbitPhaseAtEpoch": 240,
"orbitInclination": 0.07,
"orbitLongitudeOfAscendingNode": 0
}
],
"orbitRadius": 19.201,
"orbitSpeed": 0.001307,
"orbitEccentricity": 0.046,
"orbitInclination": 0.77,
"orbitLongitudeOfAscendingNode": 74,
"orbitArgumentOfPeriapsis": 97,
"orbitPhaseAtEpoch": 130,
"size": 25362,
"color": "#9fd3df",
"tilt": 1.71,
"hasRing": true
},
{
"label": "Neptune",
"planetType": "ice-giant",
"shape": "oblate",
"moons": [
{
"label": "Triton",
"size": 1353,
"color": "#b8c8d0",
"orbitRadius": 354800,
"orbitSpeed": -0.4076,
"orbitPhaseAtEpoch": 100,
"orbitInclination": 157,
"orbitLongitudeOfAscendingNode": 0
}
],
"orbitRadius": 30.047,
"orbitSpeed": 0.000668,
"orbitEccentricity": 0.009,
"orbitInclination": 1.77,
"orbitLongitudeOfAscendingNode": 132,
"orbitArgumentOfPeriapsis": 273,
"orbitPhaseAtEpoch": 256,
"size": 24622,
"color": "#4c79c9",
"tilt": 0.49,
"hasRing": true
}
]
},
{ {
"id": "helios", "id": "helios",
"label": "Helios Reach", "label": "Helios Reach",
"position": [0, 0, 0], "position": [
"starColor": "#ffd27a", 0,
"starGlow": "#ffb14a", 0,
"starSize": 720000, 0
"gravityWellRadius": 210, ],
"stars": [
{
"kind": "main-sequence",
"color": "#ffd27a",
"glow": "#ffb14a",
"size": 720000
}
],
"asteroidField": { "asteroidField": {
"decorationCount": 180, "decorationCount": 180,
"radiusOffset": 330000, "radiusOffset": 330000,
@@ -15,20 +362,57 @@
}, },
"resourceNodes": [], "resourceNodes": [],
"planets": [ "planets": [
{ "label": "Icarus", "orbitRadius": 0.36, "orbitSpeed": 0.5093, "size": 4200, "color": "#d4a373", "tilt": 0.2 }, {
{ "label": "Viridia", "orbitRadius": 0.60, "orbitSpeed": 0.2366, "size": 6200, "color": "#58a36c", "tilt": -0.4 }, "label": "Icarus",
{ "label": "Aster", "orbitRadius": 0.92, "orbitSpeed": 0.1246, "size": 7800, "color": "#6ea7d4", "tilt": 0.3, "hasRing": true }, "orbitRadius": 0.36,
{ "label": "Noctis", "orbitRadius": 1.34, "orbitSpeed": 0.0710, "size": 11200, "color": "#6958a8", "tilt": -0.15 } "orbitSpeed": 0.5093,
"size": 4200,
"color": "#d4a373",
"tilt": 0.2
},
{
"label": "Viridia",
"orbitRadius": 0.60,
"orbitSpeed": 0.2366,
"size": 6200,
"color": "#58a36c",
"tilt": -0.4
},
{
"label": "Aster",
"orbitRadius": 0.92,
"orbitSpeed": 0.1246,
"size": 7800,
"color": "#6ea7d4",
"tilt": 0.3,
"hasRing": true
},
{
"label": "Noctis",
"orbitRadius": 1.34,
"orbitSpeed": 0.0710,
"size": 11200,
"color": "#6958a8",
"tilt": -0.15
}
] ]
}, },
{ {
"id": "perseus", "id": "perseus",
"label": "Perseus Gate", "label": "Perseus Gate",
"position": [4.4, 0, 0.62], "position": [
"starColor": "#9dc6ff", 4.4,
"starGlow": "#66a0ff", 0,
"starSize": 930000, 0.62
"gravityWellRadius": 230, ],
"stars": [
{
"kind": "blue-white",
"color": "#9dc6ff",
"glow": "#66a0ff",
"size": 930000
}
],
"asteroidField": { "asteroidField": {
"decorationCount": 180, "decorationCount": 180,
"radiusOffset": 330000, "radiusOffset": 330000,
@@ -36,14 +420,54 @@
"heightVariance": 18000 "heightVariance": 18000
}, },
"resourceNodes": [ "resourceNodes": [
{ "angle": 0.45, "radiusOffset": 180000, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 }, {
{ "angle": 2.544395102, "radiusOffset": 180000, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 }, "angle": 0.45,
{ "angle": 4.638790205, "radiusOffset": 180000, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 } "radiusOffset": 180000,
"oreAmount": 3000,
"itemId": "ore",
"shardCount": 7
},
{
"angle": 2.544395102,
"radiusOffset": 180000,
"oreAmount": 3000,
"itemId": "ore",
"shardCount": 7
},
{
"angle": 4.638790205,
"radiusOffset": 180000,
"oreAmount": 3000,
"itemId": "ore",
"shardCount": 7
}
], ],
"planets": [ "planets": [
{ "label": "Talos", "orbitRadius": 0.40, "orbitSpeed": 0.4348, "size": 5000, "color": "#c48f6a", "tilt": 0.18 }, {
{ "label": "Cygnus", "orbitRadius": 0.72, "orbitSpeed": 0.1800, "size": 6900, "color": "#4f84c4", "tilt": -0.22, "hasRing": true }, "label": "Talos",
{ "label": "Rhea", "orbitRadius": 1.08, "orbitSpeed": 0.0981, "size": 9600, "color": "#8f8fb0", "tilt": 0.08 } "orbitRadius": 0.40,
"orbitSpeed": 0.4348,
"size": 5000,
"color": "#c48f6a",
"tilt": 0.18
},
{
"label": "Cygnus",
"orbitRadius": 0.72,
"orbitSpeed": 0.1800,
"size": 6900,
"color": "#4f84c4",
"tilt": -0.22,
"hasRing": true
},
{
"label": "Rhea",
"orbitRadius": 1.08,
"orbitSpeed": 0.0981,
"size": 9600,
"color": "#8f8fb0",
"tilt": 0.08
}
] ]
} }
] ]