feat: improved visualisation and x4 data import
This commit is contained in:
@@ -1,20 +1,36 @@
|
||||
namespace SpaceGame.Simulation.Api.Contracts;
|
||||
|
||||
public sealed record StarSnapshot(
|
||||
string Kind,
|
||||
string Color,
|
||||
string Glow,
|
||||
float Size,
|
||||
float OrbitRadius,
|
||||
float OrbitSpeed,
|
||||
float OrbitPhaseAtEpoch);
|
||||
|
||||
public sealed record MoonSnapshot(
|
||||
string Label,
|
||||
float Size,
|
||||
string Color,
|
||||
float OrbitRadius,
|
||||
float OrbitSpeed,
|
||||
float OrbitPhaseAtEpoch,
|
||||
float OrbitInclination,
|
||||
float OrbitLongitudeOfAscendingNode);
|
||||
|
||||
public sealed record SystemSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
Vector3Dto GalaxyPosition,
|
||||
string StarKind,
|
||||
int StarCount,
|
||||
string StarColor,
|
||||
float StarSize,
|
||||
IReadOnlyList<StarSnapshot> Stars,
|
||||
IReadOnlyList<PlanetSnapshot> Planets);
|
||||
|
||||
public sealed record PlanetSnapshot(
|
||||
string Label,
|
||||
string PlanetType,
|
||||
string Shape,
|
||||
int MoonCount,
|
||||
IReadOnlyList<MoonSnapshot> Moons,
|
||||
float OrbitRadius,
|
||||
float OrbitSpeed,
|
||||
float OrbitEccentricity,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SpaceGame.Simulation.Api.Data;
|
||||
|
||||
public sealed class ConstructionDefinition
|
||||
@@ -13,6 +15,29 @@ public sealed class ConstructionDefinition
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ItemPriceDefinition
|
||||
{
|
||||
public float Min { get; set; }
|
||||
public float Max { get; set; }
|
||||
public float Avg { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ItemEffectDefinition
|
||||
{
|
||||
public required string Type { get; set; }
|
||||
public float Product { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ItemProductionDefinition
|
||||
{
|
||||
public float Time { get; set; }
|
||||
public float Amount { get; set; }
|
||||
public string Method { get; set; } = "default";
|
||||
public string Name { get; set; } = "Universal";
|
||||
public List<RecipeInputDefinition> Wares { get; set; } = [];
|
||||
public List<ItemEffectDefinition> Effects { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class BalanceDefinition
|
||||
{
|
||||
public float YPlane { get; set; }
|
||||
@@ -25,17 +50,35 @@ public sealed class BalanceDefinition
|
||||
public float UndockDistance { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StarDefinition
|
||||
{
|
||||
public string Kind { get; set; } = "main-sequence";
|
||||
public required string Color { get; set; }
|
||||
public required string Glow { get; set; }
|
||||
public float Size { get; set; }
|
||||
public float OrbitRadius { get; set; }
|
||||
public float OrbitSpeed { get; set; }
|
||||
public float OrbitPhaseAtEpoch { get; set; }
|
||||
}
|
||||
|
||||
public sealed class MoonDefinition
|
||||
{
|
||||
public required string Label { get; set; }
|
||||
public float Size { get; set; }
|
||||
public required string Color { get; set; }
|
||||
public float OrbitRadius { get; set; }
|
||||
public float OrbitSpeed { get; set; }
|
||||
public float OrbitPhaseAtEpoch { get; set; }
|
||||
public float OrbitInclination { get; set; }
|
||||
public float OrbitLongitudeOfAscendingNode { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SolarSystemDefinition
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public required float[] Position { get; set; }
|
||||
public string StarKind { get; set; } = "main-sequence";
|
||||
public int StarCount { get; set; } = 1;
|
||||
public required string StarColor { get; set; }
|
||||
public required string StarGlow { get; set; }
|
||||
public float StarSize { get; set; }
|
||||
public float GravityWellRadius { get; set; }
|
||||
public required List<StarDefinition> Stars { get; set; }
|
||||
public required AsteroidFieldDefinition AsteroidField { get; set; }
|
||||
public required List<ResourceNodeDefinition> ResourceNodes { get; set; }
|
||||
public required List<PlanetDefinition> Planets { get; set; }
|
||||
@@ -68,9 +111,21 @@ public sealed class ItemDefinition
|
||||
public required string Name { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = "material";
|
||||
public required string CargoKind { get; set; }
|
||||
public string CargoKind { get; set; } = string.Empty;
|
||||
public float Volume { get; set; } = 1f;
|
||||
public int Version { get; set; }
|
||||
public string FactoryName { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public string Group { get; set; } = string.Empty;
|
||||
public ItemPriceDefinition? Price { get; set; }
|
||||
public List<string> Illegal { get; set; } = [];
|
||||
public List<ItemProductionDefinition> Production { get; set; } = [];
|
||||
public ConstructionDefinition? Construction { get; set; }
|
||||
[JsonPropertyName("transport")]
|
||||
public string Transport
|
||||
{
|
||||
set => CargoKind = value;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RecipeOutputDefinition
|
||||
@@ -81,8 +136,13 @@ public sealed class RecipeOutputDefinition
|
||||
|
||||
public sealed class RecipeInputDefinition
|
||||
{
|
||||
public required string ItemId { get; set; }
|
||||
public string ItemId { get; set; } = string.Empty;
|
||||
public float Amount { get; set; }
|
||||
[JsonPropertyName("ware")]
|
||||
public string Ware
|
||||
{
|
||||
set => ItemId = value;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModuleConstructionDefinition
|
||||
@@ -91,18 +151,73 @@ public sealed class ModuleConstructionDefinition
|
||||
public float ProductionTime { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ModuleDockDefinition
|
||||
{
|
||||
public int Capacity { get; set; }
|
||||
public required string Size { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ModuleCargoDefinition
|
||||
{
|
||||
public float Max { get; set; }
|
||||
public required string Type { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ModuleWorkForceDefinition
|
||||
{
|
||||
public float Capacity { get; set; }
|
||||
public float Max { get; set; }
|
||||
public string Race { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ModuleMountDefinition
|
||||
{
|
||||
public required string Group { get; set; }
|
||||
public required string Size { get; set; }
|
||||
public bool Hittable { get; set; }
|
||||
public List<string> Types { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class ModuleProductionDefinition
|
||||
{
|
||||
public float Time { get; set; }
|
||||
public float Amount { get; set; }
|
||||
public string Method { get; set; } = "default";
|
||||
public string Name { get; set; } = "Universal";
|
||||
public List<RecipeInputDefinition> Wares { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class ModuleDefinition
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public required string Type { get; set; }
|
||||
[JsonIgnore]
|
||||
public string? Product { get; set; }
|
||||
public List<string> Products { get; set; } = [];
|
||||
public string ProductionMode { get; set; } = "passive";
|
||||
public float Radius { get; set; } = 12f;
|
||||
public float Hull { get; set; } = 100f;
|
||||
public float WorkforceNeeded { get; set; }
|
||||
public int Version { get; set; }
|
||||
public string Macro { get; set; } = string.Empty;
|
||||
public string MakerRace { get; set; } = string.Empty;
|
||||
public int ExplosionDamage { get; set; }
|
||||
public ItemPriceDefinition? Price { get; set; }
|
||||
public List<string> Owners { get; set; } = [];
|
||||
public ModuleCargoDefinition? Cargo { get; set; }
|
||||
public ModuleWorkForceDefinition? WorkForce { get; set; }
|
||||
public List<ModuleDockDefinition> Docks { get; set; } = [];
|
||||
public List<ModuleMountDefinition> Shields { get; set; } = [];
|
||||
public List<ModuleMountDefinition> Turrets { get; set; } = [];
|
||||
public List<ModuleProductionDefinition> Production { get; set; } = [];
|
||||
public ModuleConstructionDefinition? Construction { get; set; }
|
||||
[JsonPropertyName("product")]
|
||||
public List<string> ProductIds
|
||||
{
|
||||
set => Products = value ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModuleRecipeDefinition
|
||||
@@ -130,7 +245,7 @@ public sealed class PlanetDefinition
|
||||
public required string Label { get; set; }
|
||||
public string PlanetType { get; set; } = "terrestrial";
|
||||
public string Shape { get; set; } = "sphere";
|
||||
public int MoonCount { get; set; }
|
||||
public List<MoonDefinition> Moons { get; set; } = [];
|
||||
public float OrbitRadius { get; set; }
|
||||
public float OrbitSpeed { get; set; }
|
||||
public float OrbitEccentricity { get; set; }
|
||||
|
||||
@@ -8,19 +8,11 @@ public sealed partial class ScenarioLoader
|
||||
private const string DevelopmentCompanionSystemId = "helios";
|
||||
|
||||
private static List<SolarSystemDefinition> InjectSpecialSystems(
|
||||
IReadOnlyList<SolarSystemDefinition> authoredSystems,
|
||||
bool includeSolSystem)
|
||||
IReadOnlyList<SolarSystemDefinition> authoredSystems)
|
||||
{
|
||||
var systems = authoredSystems
|
||||
return authoredSystems
|
||||
.Select(CloneSystemDefinition)
|
||||
.ToList();
|
||||
|
||||
if (includeSolSystem && systems.All((system) => system.Id != "sol"))
|
||||
{
|
||||
systems.Add(CreateSolSystem());
|
||||
}
|
||||
|
||||
return systems;
|
||||
}
|
||||
|
||||
private static List<SolarSystemDefinition> ExpandSystems(
|
||||
@@ -156,12 +148,16 @@ public sealed partial class ScenarioLoader
|
||||
Id = id,
|
||||
Label = label,
|
||||
Position = [position.X, position.Y, position.Z],
|
||||
StarKind = starProfile.Kind,
|
||||
StarCount = starProfile.StarCount,
|
||||
StarColor = starProfile.StarColor,
|
||||
StarGlow = starProfile.StarGlow,
|
||||
StarSize = starProfile.BaseSize + ((generatedIndex % 4) * 2f),
|
||||
GravityWellRadius = template.GravityWellRadius + ((generatedIndex % 3) * 12f),
|
||||
Stars =
|
||||
[
|
||||
new StarDefinition
|
||||
{
|
||||
Kind = starProfile.Kind,
|
||||
Color = starProfile.StarColor,
|
||||
Glow = starProfile.StarGlow,
|
||||
Size = starProfile.BaseSize + ((generatedIndex % 4) * 2f),
|
||||
},
|
||||
],
|
||||
AsteroidField = new AsteroidFieldDefinition
|
||||
{
|
||||
DecorationCount = template.AsteroidField.DecorationCount + ((generatedIndex % 5) * 10),
|
||||
@@ -181,12 +177,7 @@ public sealed partial class ScenarioLoader
|
||||
Id = definition.Id,
|
||||
Label = definition.Label,
|
||||
Position = definition.Position.ToArray(),
|
||||
StarKind = definition.StarKind,
|
||||
StarCount = definition.StarCount,
|
||||
StarColor = definition.StarColor,
|
||||
StarGlow = definition.StarGlow,
|
||||
StarSize = definition.StarSize,
|
||||
GravityWellRadius = definition.GravityWellRadius,
|
||||
Stars = definition.Stars.Select(s => new StarDefinition { Kind = s.Kind, Color = s.Color, Glow = s.Glow, Size = s.Size, OrbitRadius = s.OrbitRadius, OrbitSpeed = s.OrbitSpeed, OrbitPhaseAtEpoch = s.OrbitPhaseAtEpoch }).ToList(),
|
||||
AsteroidField = new AsteroidFieldDefinition
|
||||
{
|
||||
DecorationCount = definition.AsteroidField.DecorationCount,
|
||||
@@ -214,7 +205,7 @@ public sealed partial class ScenarioLoader
|
||||
Label = planet.Label,
|
||||
PlanetType = planet.PlanetType,
|
||||
Shape = planet.Shape,
|
||||
MoonCount = planet.MoonCount,
|
||||
Moons = planet.Moons.Select(m => new MoonDefinition { Label = m.Label, Size = m.Size, Color = m.Color, OrbitRadius = m.OrbitRadius, OrbitSpeed = m.OrbitSpeed, OrbitPhaseAtEpoch = m.OrbitPhaseAtEpoch, OrbitInclination = m.OrbitInclination, OrbitLongitudeOfAscendingNode = m.OrbitLongitudeOfAscendingNode }).ToList(),
|
||||
OrbitRadius = planet.OrbitRadius,
|
||||
OrbitSpeed = planet.OrbitSpeed,
|
||||
OrbitEccentricity = planet.OrbitEccentricity,
|
||||
@@ -387,14 +378,15 @@ public sealed partial class ScenarioLoader
|
||||
orbitRadius += profile.OrbitGapMin + (Hash01(generatedIndex, 10 + index) * (profile.OrbitGapMax - profile.OrbitGapMin));
|
||||
var orbitEccentricity = 0.01f + (Hash01(generatedIndex, 20 + index) * 0.16f);
|
||||
var orbitInclination = -9f + (Hash01(generatedIndex, 30 + index) * 18f);
|
||||
var moonVariance = (int)MathF.Floor(Hash01(generatedIndex, 40 + index) * 3f);
|
||||
var moonCount = profile.BaseMoonCount + (int)MathF.Floor(Hash01(generatedIndex, 40 + index) * 3f);
|
||||
var planetLabel = $"{BuildPlanetBaseName(generatedIndex, index)}-{index + 1}";
|
||||
|
||||
planets.Add(new PlanetDefinition
|
||||
{
|
||||
Label = $"{BuildPlanetBaseName(generatedIndex, index)}-{index + 1}",
|
||||
Label = planetLabel,
|
||||
PlanetType = profile.Type,
|
||||
Shape = profile.Shape,
|
||||
MoonCount = profile.BaseMoonCount + moonVariance,
|
||||
Moons = GenerateMoons(planetLabel, profile.BaseSize, moonCount),
|
||||
OrbitRadius = orbitRadius,
|
||||
OrbitSpeed = 0.11f / MathF.Sqrt(MathF.Max(orbitRadius * orbitRadius * orbitRadius, 0.02f)),
|
||||
OrbitEccentricity = orbitEccentricity,
|
||||
@@ -471,12 +463,44 @@ public sealed partial class ScenarioLoader
|
||||
return (value & 0x00ffffff) / 16777215f;
|
||||
}
|
||||
|
||||
private static List<MoonDefinition> GenerateMoons(string planetLabel, float planetSize, int moonCount)
|
||||
{
|
||||
var seed = planetLabel.Aggregate(0, (acc, c) => acc * 31 + c);
|
||||
var moons = new List<MoonDefinition>(moonCount);
|
||||
for (var moonIndex = 0; moonIndex < moonCount; moonIndex += 1)
|
||||
{
|
||||
var spacing = planetSize * 1.4f;
|
||||
var radiusVariance = Hash01(seed, 10 + moonIndex) * planetSize * 0.9f;
|
||||
var orbitRadius = (planetSize * 1.8f) + (moonIndex * spacing) + radiusVariance;
|
||||
var orbitSpeed = 0.9f / MathF.Sqrt(MathF.Max(orbitRadius, 1f)) + (moonIndex * 0.003f);
|
||||
var phase = Hash01(seed, 20 + moonIndex) * 360f;
|
||||
var inclination = (Hash01(seed, 30 + moonIndex) - 0.5f) * 28f;
|
||||
var ascendingNode = Hash01(seed, 40 + moonIndex) * 360f;
|
||||
var sizeBase = MathF.Max(2.2f, planetSize * 0.11f);
|
||||
var sizeVariance = Hash01(seed, 50 + moonIndex) * MathF.Max(planetSize * 0.16f, 2.5f);
|
||||
var size = MathF.Min(sizeBase + sizeVariance, planetSize * 0.42f);
|
||||
|
||||
moons.Add(new MoonDefinition
|
||||
{
|
||||
Label = $"{planetLabel}-m{moonIndex + 1}",
|
||||
Size = size,
|
||||
Color = "#c8c4bc",
|
||||
OrbitRadius = orbitRadius,
|
||||
OrbitSpeed = orbitSpeed,
|
||||
OrbitPhaseAtEpoch = phase,
|
||||
OrbitInclination = inclination,
|
||||
OrbitLongitudeOfAscendingNode = ascendingNode,
|
||||
});
|
||||
}
|
||||
|
||||
return moons;
|
||||
}
|
||||
|
||||
private sealed record StarProfile(
|
||||
string Kind,
|
||||
string StarColor,
|
||||
string StarGlow,
|
||||
float BaseSize,
|
||||
int StarCount);
|
||||
float BaseSize);
|
||||
|
||||
private sealed record PlanetProfile(
|
||||
string Type,
|
||||
@@ -490,106 +514,4 @@ public sealed partial class ScenarioLoader
|
||||
public float OrbitGapMax => OrbitGapMin + MathF.Max(0.12f, OrbitGapMin * 0.45f);
|
||||
}
|
||||
|
||||
private static SolarSystemDefinition CreateSolSystem()
|
||||
{
|
||||
var mercuryOrbitAu = 0.3871f;
|
||||
var venusOrbitAu = 0.7233f;
|
||||
var earthOrbitAu = 1.000f;
|
||||
var marsOrbitAu = 1.5237f;
|
||||
var jupiterOrbitAu = 5.203f;
|
||||
var saturnOrbitAu = 9.582f;
|
||||
var uranusOrbitAu = 19.201f;
|
||||
var neptuneOrbitAu = 30.047f;
|
||||
|
||||
return new SolarSystemDefinition
|
||||
{
|
||||
Id = "sol",
|
||||
Label = "Sol",
|
||||
Position = [18.2f, 0.02f, -11.8f],
|
||||
StarKind = "main-sequence",
|
||||
StarCount = 1,
|
||||
StarColor = "#fff1b8",
|
||||
StarGlow = "#ffd35a",
|
||||
StarSize = 696340f,
|
||||
GravityWellRadius = 240f,
|
||||
AsteroidField = new AsteroidFieldDefinition
|
||||
{
|
||||
DecorationCount = 240,
|
||||
RadiusOffset = 422000000f,
|
||||
RadiusVariance = 180000000f,
|
||||
HeightVariance = 22000000f,
|
||||
},
|
||||
ResourceNodes =
|
||||
[
|
||||
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 0.2f, RadiusOffset = 126000f, InclinationDegrees = 4f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
|
||||
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 1.8f, RadiusOffset = 148000f, InclinationDegrees = -6f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
|
||||
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 3.5f, RadiusOffset = 138000f, InclinationDegrees = 8f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
|
||||
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 5.1f, RadiusOffset = 164000f, InclinationDegrees = -5f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
|
||||
],
|
||||
Planets =
|
||||
[
|
||||
CreateSolPlanet("Mercury", "barren", "sphere", 0, mercuryOrbitAu, 0.2056f, 7.0f, 48f, 29f, 252f, "#b7a08f", 0.03f, false),
|
||||
CreateSolPlanet("Venus", "desert", "sphere", 0, venusOrbitAu, 0.0067f, 3.4f, 76f, 54f, 181f, "#d9b38c", 2.64f, false),
|
||||
CreateSolPlanet("Earth", "terrestrial", "sphere", 1, earthOrbitAu, 0.0167f, 0.0f, 0f, 114f, 100f, "#4f84c4", 0.41f, false),
|
||||
CreateSolPlanet("Mars", "desert", "sphere", 2, marsOrbitAu, 0.0934f, 1.85f, 49f, 286f, 54f, "#c56e52", 0.44f, false),
|
||||
CreateSolPlanet("Jupiter", "gas-giant", "oblate", 95, jupiterOrbitAu, 0.0489f, 1.3f, 100f, 275f, 34f, "#d9b06f", 0.05f, true),
|
||||
CreateSolPlanet("Saturn", "gas-giant", "oblate", 146, saturnOrbitAu, 0.0565f, 2.49f, 113f, 339f, 200f, "#dfc27d", 0.47f, true),
|
||||
CreateSolPlanet("Uranus", "ice-giant", "oblate", 28, uranusOrbitAu, 0.046f, 0.77f, 74f, 97f, 130f, "#9fd3df", 1.71f, true),
|
||||
CreateSolPlanet("Neptune", "ice-giant", "oblate", 16, neptuneOrbitAu, 0.009f, 1.77f, 132f, 273f, 256f, "#4c79c9", 0.49f, true)
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private static PlanetDefinition CreateSolPlanet(
|
||||
string label,
|
||||
string planetType,
|
||||
string shape,
|
||||
int moonCount,
|
||||
float orbitRadiusAu,
|
||||
float orbitEccentricity,
|
||||
float orbitInclination,
|
||||
float ascendingNode,
|
||||
float argumentOfPeriapsis,
|
||||
float phaseAtEpoch,
|
||||
string color,
|
||||
float tilt,
|
||||
bool hasRing)
|
||||
{
|
||||
return new PlanetDefinition
|
||||
{
|
||||
Label = label,
|
||||
PlanetType = planetType,
|
||||
Shape = shape,
|
||||
MoonCount = moonCount,
|
||||
OrbitRadius = orbitRadiusAu,
|
||||
OrbitSpeed = ComputeSolOrbitSpeed(orbitRadiusAu),
|
||||
OrbitEccentricity = orbitEccentricity,
|
||||
OrbitInclination = orbitInclination,
|
||||
OrbitLongitudeOfAscendingNode = ascendingNode,
|
||||
OrbitArgumentOfPeriapsis = argumentOfPeriapsis,
|
||||
OrbitPhaseAtEpoch = phaseAtEpoch,
|
||||
Size = planetType switch
|
||||
{
|
||||
"gas-giant" => label == "Saturn" ? 58232f : 69911f,
|
||||
"ice-giant" => label == "Uranus" ? 25362f : 24622f,
|
||||
_ => label switch
|
||||
{
|
||||
"Mercury" => 2440f,
|
||||
"Venus" => 6052f,
|
||||
"Earth" => 6371f,
|
||||
"Mars" => 3390f,
|
||||
_ => 5000f,
|
||||
},
|
||||
},
|
||||
Color = color,
|
||||
Tilt = tilt,
|
||||
HasRing = hasRing,
|
||||
};
|
||||
}
|
||||
|
||||
private static float ComputeSolOrbitSpeed(float orbitRadiusAu)
|
||||
{
|
||||
const float earthAngularSpeed = 0.11f;
|
||||
return earthAngularSpeed / MathF.Sqrt(orbitRadiusAu * orbitRadiusAu * orbitRadiusAu);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +60,14 @@ public sealed partial class ScenarioLoader
|
||||
.ToList();
|
||||
|
||||
var refineries = ownedStations
|
||||
.Where((station) => HasInstalledModules(station, "refinery-stack", "power-core", "liquid-tank"))
|
||||
.Where((station) => HasInstalledModules(station, "module_gen_prod_refinedmetals_01", "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01"))
|
||||
.ToList();
|
||||
|
||||
if (refineries.Count > 0)
|
||||
{
|
||||
foreach (var refinery in refineries)
|
||||
{
|
||||
refinery.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(refinery.Inventory, "refined-metals"), MinimumRefineryStock);
|
||||
refinery.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(refinery.Inventory, "refinedmetals"), MinimumRefineryStock);
|
||||
}
|
||||
|
||||
if (refineries.All((station) => GetInventoryAmount(station.Inventory, "ore") < MinimumRefineryOre))
|
||||
@@ -76,9 +76,9 @@ public sealed partial class ScenarioLoader
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var shipyard in ownedStations.Where((station) => HasInstalledModules(station, "ship-factory")))
|
||||
foreach (var shipyard in ownedStations.Where((station) => HasInstalledModules(station, "module_gen_build_l_01")))
|
||||
{
|
||||
shipyard.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refined-metals"), MinimumShipyardStock);
|
||||
shipyard.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refinedmetals"), MinimumShipyardStock);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,13 +189,13 @@ public sealed partial class ScenarioLoader
|
||||
{
|
||||
foreach (var (moduleId, targetCount) in new (string ModuleId, int TargetCount)[]
|
||||
{
|
||||
("refinery-stack", 1),
|
||||
("container-bay", 1),
|
||||
("fabricator-array", 2),
|
||||
("component-factory", 1),
|
||||
("ship-factory", 1),
|
||||
("solar-array", 2),
|
||||
("dock-bay-small", 2),
|
||||
("module_gen_prod_refinedmetals_01", 1),
|
||||
("module_arg_stor_container_m_01", 1),
|
||||
("module_gen_prod_hullparts_01", 2),
|
||||
("module_gen_prod_advancedelectronics_01", 1),
|
||||
("module_gen_build_l_01", 1),
|
||||
("module_gen_prod_energycells_01", 2),
|
||||
("module_arg_dock_m_01_lowtech", 2),
|
||||
})
|
||||
{
|
||||
if (CountModules(station.InstalledModules, moduleId) < targetCount
|
||||
@@ -210,7 +210,7 @@ public sealed partial class ScenarioLoader
|
||||
|
||||
private static void InitializeStationPopulation(StationRuntime station)
|
||||
{
|
||||
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
|
||||
var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01");
|
||||
station.PopulationCapacity = 40f + (habitatModules * 220f);
|
||||
station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
|
||||
station.Population = habitatModules > 0
|
||||
|
||||
@@ -9,13 +9,18 @@ public sealed partial class ScenarioLoader
|
||||
var celestials = new List<CelestialRuntime>();
|
||||
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();
|
||||
|
||||
AddCelestial(
|
||||
celestials,
|
||||
id: $"node-{system.Definition.Id}-star",
|
||||
systemId: system.Definition.Id,
|
||||
kind: SpatialNodeKind.Star,
|
||||
position: Vector3.Zero,
|
||||
localSpaceRadius: MathF.Max(system.Definition.GravityWellRadius + StarBubbleRadiusPadding, LocalSpaceRadius));
|
||||
for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
|
||||
{
|
||||
AddCelestial(
|
||||
celestials,
|
||||
id: $"node-{system.Definition.Id}-star-{starIndex + 1}",
|
||||
systemId: system.Definition.Id,
|
||||
kind: SpatialNodeKind.Star,
|
||||
position: Vector3.Zero,
|
||||
localSpaceRadius: LocalSpaceRadius);
|
||||
}
|
||||
|
||||
var primaryStarNodeId = $"node-{system.Definition.Id}-star-1";
|
||||
|
||||
for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
|
||||
{
|
||||
@@ -29,7 +34,7 @@ public sealed partial class ScenarioLoader
|
||||
kind: SpatialNodeKind.Planet,
|
||||
position: planetPosition,
|
||||
localSpaceRadius: LocalSpaceRadius,
|
||||
parentNodeId: $"node-{system.Definition.Id}-star");
|
||||
parentNodeId: primaryStarNodeId);
|
||||
|
||||
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
|
||||
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
|
||||
@@ -48,15 +53,10 @@ public sealed partial class ScenarioLoader
|
||||
|
||||
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
|
||||
|
||||
if (planet.MoonCount <= 0)
|
||||
for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var moonOrbitRadius = MathF.Max(planet.Size + 48f, 42f);
|
||||
for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1)
|
||||
{
|
||||
var moonPosition = ComputeMoonPosition(planetPosition, moonOrbitRadius, moonIndex, planetIndex);
|
||||
var moon = planet.Moons[moonIndex];
|
||||
var moonPosition = ComputeMoonPosition(planetPosition, moon);
|
||||
AddCelestial(
|
||||
celestials,
|
||||
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
|
||||
@@ -65,7 +65,6 @@ public sealed partial class ScenarioLoader
|
||||
position: moonPosition,
|
||||
localSpaceRadius: LocalSpaceRadius,
|
||||
parentNodeId: planetCelestial.Id);
|
||||
moonOrbitRadius += 30f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,10 +231,11 @@ public sealed partial class ScenarioLoader
|
||||
return new Vector3(x, 0f, z);
|
||||
}
|
||||
|
||||
private static Vector3 ComputeMoonPosition(Vector3 planetPosition, float orbitRadius, int moonIndex, int planetIndex)
|
||||
private static Vector3 ComputeMoonPosition(Vector3 planetPosition, MoonDefinition moon)
|
||||
{
|
||||
var angle = ((MathF.PI * 2f) / MathF.Max(1, moonIndex + 3)) * (moonIndex + 1) + (planetIndex * 0.37f);
|
||||
return Add(planetPosition, new Vector3(MathF.Cos(angle) * orbitRadius, 0f, MathF.Sin(angle) * orbitRadius));
|
||||
var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch);
|
||||
var local = new Vector3(MathF.Cos(angle) * moon.OrbitRadius, 0f, MathF.Sin(angle) * moon.OrbitRadius);
|
||||
return Add(planetPosition, local);
|
||||
}
|
||||
|
||||
private static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<CelestialRuntime> celestials)
|
||||
|
||||
@@ -12,7 +12,6 @@ public sealed partial class ScenarioLoader
|
||||
private const float MinimumRefineryStock = 0f;
|
||||
private const float MinimumShipyardStock = 0f;
|
||||
private const float MinimumSystemSeparation = 3.2f;
|
||||
private const float StarBubbleRadiusPadding = 40f;
|
||||
private const float LocalSpaceRadius = 10_000f;
|
||||
private static readonly string[] GeneratedSystemNames =
|
||||
[
|
||||
@@ -51,13 +50,13 @@ public sealed partial class ScenarioLoader
|
||||
];
|
||||
private static readonly StarProfile[] StarProfiles =
|
||||
[
|
||||
new("main-sequence", "#ffd27a", "#ffb14a", 696340f, 1),
|
||||
new("blue-white", "#9dc6ff", "#66a0ff", 930000f, 1),
|
||||
new("white-dwarf", "#f1f5ff", "#b8caff", 12000f, 1),
|
||||
new("brown-dwarf", "#b97d56", "#8a5438", 70000f, 1),
|
||||
new("neutron-star", "#d9ebff", "#7ab4ff", 18f, 1),
|
||||
new("binary-main-sequence", "#ffe09f", "#ffbe6b", 780000f, 2),
|
||||
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f, 2),
|
||||
new("main-sequence", "#ffd27a", "#ffb14a", 696340f),
|
||||
new("blue-white", "#9dc6ff", "#66a0ff", 930000f),
|
||||
new("white-dwarf", "#f1f5ff", "#b8caff", 12000f),
|
||||
new("brown-dwarf", "#b97d56", "#8a5438", 70000f),
|
||||
new("neutron-star", "#d9ebff", "#7ab4ff", 18f),
|
||||
new("binary-main-sequence", "#ffe09f", "#ffbe6b", 780000f),
|
||||
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f),
|
||||
];
|
||||
private static readonly PlanetProfile[] PlanetProfiles =
|
||||
[
|
||||
@@ -88,16 +87,16 @@ public sealed partial class ScenarioLoader
|
||||
{
|
||||
var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json");
|
||||
var systems = ExpandSystems(
|
||||
InjectSpecialSystems(authoredSystems, _worldGeneration.IncludeSolSystem),
|
||||
InjectSpecialSystems(authoredSystems),
|
||||
_worldGeneration.TargetSystemCount);
|
||||
var scenario = NormalizeScenarioToAvailableSystems(
|
||||
Read<ScenarioDefinition>("scenario.json"),
|
||||
systems.Select((system) => system.Id).ToList());
|
||||
var modules = Read<List<ModuleDefinition>>("modules.json");
|
||||
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
|
||||
var ships = Read<List<ShipDefinition>>("ships.json");
|
||||
var items = Read<List<ItemDefinition>>("items.json");
|
||||
var items = NormalizeItems(Read<List<ItemDefinition>>("items.json"));
|
||||
var balance = Read<BalanceDefinition>("balance.json");
|
||||
var recipes = BuildRecipes(items, ships);
|
||||
var recipes = BuildRecipes(items, ships, modules);
|
||||
var moduleRecipes = BuildModuleRecipes(modules);
|
||||
|
||||
var moduleDefinitions = modules.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||
@@ -177,7 +176,7 @@ public sealed partial class ScenarioLoader
|
||||
|
||||
var startingModules = plan.StartingModules.Count > 0
|
||||
? plan.StartingModules
|
||||
: ["dock-bay-small", "power-core", "bulk-bay", "liquid-tank"];
|
||||
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01", "module_arg_stor_liquid_m_01"];
|
||||
foreach (var moduleId in startingModules)
|
||||
{
|
||||
AddStationModule(stations[^1], moduleDefinitions, moduleId);
|
||||
@@ -187,7 +186,7 @@ public sealed partial class ScenarioLoader
|
||||
foreach (var station in stations)
|
||||
{
|
||||
InitializeStationPopulation(station);
|
||||
station.Inventory["refined-metals"] = 120f;
|
||||
station.Inventory["refinedmetals"] = 120f;
|
||||
if (station.Population > 0f)
|
||||
{
|
||||
station.Inventory["water"] = MathF.Max(80f, station.Population * 1.5f);
|
||||
@@ -195,9 +194,9 @@ public sealed partial class ScenarioLoader
|
||||
}
|
||||
|
||||
var refinery = stations.FirstOrDefault((station) =>
|
||||
HasInstalledModules(station, "power-core", "liquid-tank") &&
|
||||
HasInstalledModules(station, "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01") &&
|
||||
station.SystemId == scenario.MiningDefaults.RefinerySystemId)
|
||||
?? stations.FirstOrDefault((station) => HasInstalledModules(station, "power-core", "liquid-tank"));
|
||||
?? stations.FirstOrDefault((station) => HasInstalledModules(station, "module_gen_prod_energycells_01", "module_arg_stor_liquid_m_01"));
|
||||
|
||||
var patrolRoutes = scenario.PatrolRoutes
|
||||
.GroupBy((route) => route.SystemId, StringComparer.Ordinal)
|
||||
@@ -400,12 +399,12 @@ public sealed partial class ScenarioLoader
|
||||
|
||||
private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
|
||||
modules
|
||||
.Where((module) => module.Construction is not null)
|
||||
.Where((module) => module.Construction is not null || module.Production.Count > 0)
|
||||
.Select((module) => new ModuleRecipeDefinition
|
||||
{
|
||||
ModuleId = module.Id,
|
||||
Duration = module.Construction!.ProductionTime,
|
||||
Inputs = module.Construction.Requirements
|
||||
Duration = module.Construction?.ProductionTime ?? module.Production[0].Time,
|
||||
Inputs = (module.Construction?.Requirements ?? module.Production[0].Wares)
|
||||
.Select((input) => new RecipeInputDefinition
|
||||
{
|
||||
ItemId = input.ItemId,
|
||||
@@ -415,12 +414,54 @@ public sealed partial class ScenarioLoader
|
||||
})
|
||||
.ToList();
|
||||
|
||||
private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships)
|
||||
private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships, IReadOnlyCollection<ModuleDefinition> modules)
|
||||
{
|
||||
var recipes = new List<RecipeDefinition>();
|
||||
var preferredProducerByItemId = modules
|
||||
.Where((module) => module.Products.Count > 0)
|
||||
.GroupBy((module) => module.Products[0], StringComparer.Ordinal)
|
||||
.ToDictionary(
|
||||
(group) => group.Key,
|
||||
(group) => group.OrderBy((module) => module.Id, StringComparer.Ordinal).First().Id,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Production.Count > 0)
|
||||
{
|
||||
foreach (var production in item.Production)
|
||||
{
|
||||
recipes.Add(new RecipeDefinition
|
||||
{
|
||||
Id = $"{item.Id}-{production.Method}-production",
|
||||
Label = production.Name == "Universal"
|
||||
? item.Name
|
||||
: $"{item.Name} ({production.Name})",
|
||||
FacilityCategory = InferFacilityCategory(item),
|
||||
Duration = production.Time,
|
||||
Priority = InferRecipePriority(item),
|
||||
RequiredModules = InferRequiredModules(item, preferredProducerByItemId),
|
||||
Inputs = production.Wares
|
||||
.Select((input) => new RecipeInputDefinition
|
||||
{
|
||||
ItemId = input.ItemId,
|
||||
Amount = input.Amount,
|
||||
})
|
||||
.ToList(),
|
||||
Outputs =
|
||||
[
|
||||
new RecipeOutputDefinition
|
||||
{
|
||||
ItemId = item.Id,
|
||||
Amount = production.Amount,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.Construction is null)
|
||||
{
|
||||
continue;
|
||||
@@ -481,6 +522,74 @@ public sealed partial class ScenarioLoader
|
||||
return recipes;
|
||||
}
|
||||
|
||||
private static string InferFacilityCategory(ItemDefinition item) =>
|
||||
item.Group switch
|
||||
{
|
||||
"agricultural" or "food" or "pharmaceutical" or "water" => "farm",
|
||||
_ => "station",
|
||||
};
|
||||
|
||||
private static List<string> InferRequiredModules(ItemDefinition item, IReadOnlyDictionary<string, string> preferredProducerByItemId)
|
||||
{
|
||||
if (preferredProducerByItemId.TryGetValue(item.Id, out var moduleId))
|
||||
{
|
||||
return [moduleId];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private static int InferRecipePriority(ItemDefinition item) =>
|
||||
item.Group switch
|
||||
{
|
||||
"energy" => 140,
|
||||
"water" => 130,
|
||||
"food" => 120,
|
||||
"agricultural" => 110,
|
||||
"refined" => 100,
|
||||
"hightech" => 90,
|
||||
"shiptech" => 80,
|
||||
"pharmaceutical" => 70,
|
||||
_ => 60,
|
||||
};
|
||||
|
||||
private static List<ItemDefinition> NormalizeItems(List<ItemDefinition> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.Type))
|
||||
{
|
||||
item.Type = string.IsNullOrWhiteSpace(item.Group) ? "material" : item.Group;
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
|
||||
{
|
||||
foreach (var module in modules)
|
||||
{
|
||||
if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product))
|
||||
{
|
||||
module.Products = [module.Product];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(module.ProductionMode))
|
||||
{
|
||||
module.ProductionMode = string.Equals(module.Type, "buildmodule", StringComparison.Ordinal)
|
||||
? "commanded"
|
||||
: "passive";
|
||||
}
|
||||
|
||||
if (module.WorkforceNeeded <= 0f)
|
||||
{
|
||||
module.WorkforceNeeded = module.WorkForce?.Max ?? 0f;
|
||||
}
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
private static Vector3 Scale(Vector3 vector, float scale) => new(vector.X * scale, vector.Y * scale, vector.Z * scale);
|
||||
|
||||
private static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f);
|
||||
|
||||
@@ -141,9 +141,9 @@ public sealed partial class SimulationEngine
|
||||
string.Equals(s.Definition.Kind, "construction", StringComparison.Ordinal)),
|
||||
ControlledSystemCount = GetFactionControlledSystemsCount(world, factionId),
|
||||
TargetSystemCount = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)),
|
||||
HasShipFactory = stations.Any(s => s.InstalledModules.Contains("ship-factory", StringComparer.Ordinal)),
|
||||
HasShipFactory = stations.Any(s => s.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
|
||||
OreStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "ore")),
|
||||
RefinedMetalsStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "refined-metals")),
|
||||
RefinedMetalsStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "refinedmetals")),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,37 +24,18 @@ public sealed partial class SimulationEngine
|
||||
return local;
|
||||
}
|
||||
|
||||
private static Vector3 ComputeMoonOffset(PlanetDefinition planet, int moonIndex, float timeSeconds)
|
||||
private static Vector3 ComputeMoonOffset(MoonDefinition moon, float timeSeconds)
|
||||
{
|
||||
var orbitRadius = ComputeMoonOrbitRadius(planet, moonIndex);
|
||||
var speed = ComputeMoonOrbitSpeed(planet, moonIndex);
|
||||
var phase = HashUnit($"{planet.Label}:{moonIndex}:phase") * MathF.PI * 2f;
|
||||
var inclination = DegreesToRadians((HashUnit($"{planet.Label}:{moonIndex}:inclination") - 0.5f) * 28f);
|
||||
var ascendingNode = DegreesToRadians(HashUnit($"{planet.Label}:{moonIndex}:node") * 360f);
|
||||
var angle = phase + (timeSeconds * speed);
|
||||
|
||||
var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch) + (timeSeconds * moon.OrbitSpeed);
|
||||
var local = new Vector3(
|
||||
MathF.Cos(angle) * orbitRadius,
|
||||
MathF.Cos(angle) * moon.OrbitRadius,
|
||||
0f,
|
||||
MathF.Sin(angle) * orbitRadius);
|
||||
local = RotateAroundX(local, inclination);
|
||||
local = RotateAroundY(local, ascendingNode);
|
||||
MathF.Sin(angle) * moon.OrbitRadius);
|
||||
local = RotateAroundX(local, DegreesToRadians(moon.OrbitInclination));
|
||||
local = RotateAroundY(local, DegreesToRadians(moon.OrbitLongitudeOfAscendingNode));
|
||||
return local;
|
||||
}
|
||||
|
||||
private static float ComputeMoonOrbitRadius(PlanetDefinition planet, int moonIndex)
|
||||
{
|
||||
var spacing = planet.Size * 1.4f;
|
||||
var variance = HashUnit($"{planet.Label}:{moonIndex}:radius") * planet.Size * 0.9f;
|
||||
return (planet.Size * 1.8f) + (moonIndex * spacing) + variance;
|
||||
}
|
||||
|
||||
private static float ComputeMoonOrbitSpeed(PlanetDefinition planet, int moonIndex)
|
||||
{
|
||||
var radius = ComputeMoonOrbitRadius(planet, moonIndex);
|
||||
return 0.9f / MathF.Sqrt(MathF.Max(radius, 1f)) + (moonIndex * 0.003f);
|
||||
}
|
||||
|
||||
private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node)
|
||||
{
|
||||
var baseSpeed = 0.24f;
|
||||
@@ -179,10 +160,24 @@ public sealed partial class SimulationEngine
|
||||
|
||||
foreach (var system in world.Systems)
|
||||
{
|
||||
var starNodeId = $"node-{system.Definition.Id}-star";
|
||||
if (celestialsById.TryGetValue(starNodeId, out var starNode))
|
||||
for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
|
||||
{
|
||||
starNode.Position = Vector3.Zero;
|
||||
var star = system.Definition.Stars[starIndex];
|
||||
var starNodeId = $"node-{system.Definition.Id}-star-{starIndex + 1}";
|
||||
if (!celestialsById.TryGetValue(starNodeId, out var starNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (star.OrbitRadius <= 0f)
|
||||
{
|
||||
starNode.Position = Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
var angle = DegreesToRadians(star.OrbitPhaseAtEpoch) + (worldTimeSeconds * star.OrbitSpeed);
|
||||
starNode.Position = new Vector3(MathF.Cos(angle) * star.OrbitRadius, 0f, MathF.Sin(angle) * star.OrbitRadius);
|
||||
}
|
||||
}
|
||||
|
||||
for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
|
||||
@@ -206,7 +201,7 @@ public sealed partial class SimulationEngine
|
||||
}
|
||||
}
|
||||
|
||||
for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1)
|
||||
for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
|
||||
{
|
||||
var moonId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
|
||||
if (!celestialsById.TryGetValue(moonId, out var moonNode))
|
||||
@@ -214,7 +209,7 @@ public sealed partial class SimulationEngine
|
||||
continue;
|
||||
}
|
||||
|
||||
moonNode.Position = Add(planetPosition, ComputeMoonOffset(planet, moonIndex, worldTimeSeconds));
|
||||
moonNode.Position = Add(planetPosition, ComputeMoonOffset(planet.Moons[moonIndex], worldTimeSeconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ public sealed partial class SimulationEngine
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
var bulkBays = CountStationModules(station, "bulk-bay");
|
||||
var liquidTanks = CountStationModules(station, "liquid-tank");
|
||||
var containerBays = CountStationModules(station, "container-bay");
|
||||
var bulkBays = CountStationModules(station, "module_arg_stor_solid_m_01");
|
||||
var liquidTanks = CountStationModules(station, "module_arg_stor_liquid_m_01");
|
||||
var containerBays = CountStationModules(station, "module_arg_stor_container_m_01");
|
||||
|
||||
var moduleCapacity = storageClass switch
|
||||
{
|
||||
@@ -118,8 +118,8 @@ public sealed partial class SimulationEngine
|
||||
private static string? GetStorageRequirement(string storageClass) =>
|
||||
storageClass switch
|
||||
{
|
||||
"solid" => "bulk-bay",
|
||||
"liquid" => "liquid-tank",
|
||||
"solid" => "module_arg_stor_solid_m_01",
|
||||
"liquid" => "module_arg_stor_liquid_m_01",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
|
||||
@@ -20,15 +20,27 @@ public sealed partial class SimulationEngine
|
||||
system.Definition.Id,
|
||||
system.Definition.Label,
|
||||
ToDto(system.Position),
|
||||
system.Definition.StarKind,
|
||||
system.Definition.StarCount,
|
||||
system.Definition.StarColor,
|
||||
system.Definition.StarSize,
|
||||
system.Definition.Stars.Select(star => new StarSnapshot(
|
||||
star.Kind,
|
||||
star.Color,
|
||||
star.Glow,
|
||||
star.Size,
|
||||
star.OrbitRadius,
|
||||
star.OrbitSpeed,
|
||||
star.OrbitPhaseAtEpoch)).ToList(),
|
||||
system.Definition.Planets.Select(planet => new PlanetSnapshot(
|
||||
planet.Label,
|
||||
planet.PlanetType,
|
||||
planet.Shape,
|
||||
planet.MoonCount,
|
||||
planet.Moons.Select(moon => new MoonSnapshot(
|
||||
moon.Label,
|
||||
moon.Size,
|
||||
moon.Color,
|
||||
moon.OrbitRadius,
|
||||
moon.OrbitSpeed,
|
||||
moon.OrbitPhaseAtEpoch,
|
||||
moon.OrbitInclination,
|
||||
moon.OrbitLongitudeOfAscendingNode)).ToList(),
|
||||
planet.OrbitRadius,
|
||||
planet.OrbitSpeed,
|
||||
planet.OrbitEccentricity,
|
||||
|
||||
@@ -110,9 +110,9 @@ public sealed partial class SimulationEngine
|
||||
const float StorageExpansionThreshold = 0.85f;
|
||||
var storageExpansionCandidates = new[]
|
||||
{
|
||||
("solid", "bulk-bay"),
|
||||
("liquid", "liquid-tank"),
|
||||
("container", "container-bay"),
|
||||
("solid", "module_arg_stor_solid_m_01"),
|
||||
("liquid", "module_arg_stor_liquid_m_01"),
|
||||
("container", "module_arg_stor_container_m_01"),
|
||||
};
|
||||
|
||||
foreach (var (storageClass, moduleId) in storageExpansionCandidates)
|
||||
@@ -136,25 +136,25 @@ public sealed partial class SimulationEngine
|
||||
var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f
|
||||
? new (string ModuleId, int TargetCount)[]
|
||||
{
|
||||
("refinery-stack", 1),
|
||||
("bulk-bay", 1),
|
||||
("container-bay", 1),
|
||||
("fabricator-array", 2),
|
||||
("component-factory", 1),
|
||||
("ship-factory", 1),
|
||||
("dock-bay-small", 2),
|
||||
("solar-array", 2),
|
||||
("module_gen_prod_refinedmetals_01", 1),
|
||||
("module_arg_stor_solid_m_01", 1),
|
||||
("module_arg_stor_container_m_01", 1),
|
||||
("module_gen_prod_hullparts_01", 2),
|
||||
("module_gen_prod_advancedelectronics_01", 1),
|
||||
("module_gen_build_l_01", 1),
|
||||
("module_arg_dock_m_01_lowtech", 2),
|
||||
("module_gen_prod_energycells_01", 2),
|
||||
}
|
||||
: new (string ModuleId, int TargetCount)[]
|
||||
{
|
||||
("refinery-stack", 1),
|
||||
("bulk-bay", 1),
|
||||
("container-bay", 1),
|
||||
("fabricator-array", 2),
|
||||
("component-factory", 1),
|
||||
("ship-factory", 1),
|
||||
("solar-array", 2),
|
||||
("dock-bay-small", 2),
|
||||
("module_gen_prod_refinedmetals_01", 1),
|
||||
("module_arg_stor_solid_m_01", 1),
|
||||
("module_arg_stor_container_m_01", 1),
|
||||
("module_gen_prod_hullparts_01", 2),
|
||||
("module_gen_prod_advancedelectronics_01", 1),
|
||||
("module_gen_build_l_01", 1),
|
||||
("module_gen_prod_energycells_01", 2),
|
||||
("module_arg_dock_m_01_lowtech", 2),
|
||||
};
|
||||
|
||||
foreach (var (moduleId, targetCount) in priorities)
|
||||
@@ -225,7 +225,7 @@ public sealed partial class SimulationEngine
|
||||
}
|
||||
|
||||
private static int GetDockingPadCount(StationRuntime station) =>
|
||||
CountModules(station.InstalledModules, "dock-bay-small") * 2;
|
||||
CountModules(station.InstalledModules, "module_arg_dock_m_01_lowtech") * 2;
|
||||
|
||||
private static int? ReserveDockingPad(StationRuntime station, string shipId)
|
||||
{
|
||||
|
||||
@@ -14,22 +14,22 @@ public sealed partial class SimulationEngine
|
||||
|
||||
var desiredOrders = new List<DesiredMarketOrder>();
|
||||
var waterReserve = MathF.Max(30f, station.Population * 3f);
|
||||
var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f;
|
||||
var refinedReserve = HasStationModules(station, "module_gen_prod_hullparts_01") ? 140f : 40f;
|
||||
var oreReserve = HasRefineryCapability(station) ? 180f : 0f;
|
||||
var shipPartsReserve = HasStationModules(station, "fabricator-array")
|
||||
&& !HasStationModules(station, "component-factory", "ship-factory")
|
||||
var shipPartsReserve = HasStationModules(station, "module_gen_prod_hullparts_01")
|
||||
&& !HasStationModules(station, "module_gen_prod_advancedelectronics_01", "module_gen_build_l_01")
|
||||
&& FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships")
|
||||
? 90f
|
||||
: 0f;
|
||||
|
||||
AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f);
|
||||
AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f);
|
||||
AddDemandOrder(desiredOrders, station, "refined-metals", refinedReserve, valuationBase: 1.15f);
|
||||
AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f);
|
||||
AddDemandOrder(desiredOrders, station, "refinedmetals", refinedReserve, valuationBase: 1.15f);
|
||||
AddDemandOrder(desiredOrders, station, "hullparts", shipPartsReserve, valuationBase: 1.3f);
|
||||
|
||||
AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f);
|
||||
AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f);
|
||||
AddSupplyOrder(desiredOrders, station, "refined-metals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
|
||||
AddSupplyOrder(desiredOrders, station, "refinedmetals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
|
||||
|
||||
ReconcileStationMarketOrders(world, station, desiredOrders);
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public sealed partial class SimulationEngine
|
||||
var fleetPressure = FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships") ? 1f : 0f;
|
||||
priority += recipe.Id switch
|
||||
{
|
||||
"ship-parts-integration" => HasStationModules(station, "component-factory", "ship-factory")
|
||||
"ship-parts-integration" => HasStationModules(station, "module_gen_prod_advancedelectronics_01", "module_gen_build_l_01")
|
||||
? -140f * MathF.Max(expansionPressure, fleetPressure)
|
||||
: 280f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"hull-fabrication" => 180f * expansionPressure,
|
||||
@@ -211,7 +211,7 @@ public sealed partial class SimulationEngine
|
||||
}
|
||||
|
||||
private static bool HasRefineryCapability(StationRuntime station) =>
|
||||
HasStationModules(station, "refinery-stack", "power-core", "bulk-bay");
|
||||
HasStationModules(station, "module_gen_prod_refinedmetals_01", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01");
|
||||
|
||||
private static void AddDemandOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase)
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed partial class SimulationEngine
|
||||
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
|
||||
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
|
||||
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
|
||||
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
|
||||
var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01");
|
||||
station.PopulationCapacity = 40f + (habitatModules * 220f);
|
||||
|
||||
if (waterSatisfied)
|
||||
|
||||
@@ -4,5 +4,4 @@ public sealed class WorldGenerationOptions
|
||||
{
|
||||
public int TargetSystemCount { get; init; } = 160;
|
||||
|
||||
public bool IncludeSolSystem { get; init; } = true;
|
||||
}
|
||||
|
||||
@@ -28,19 +28,12 @@ import { SystemLayer } from "./viewerSystemLayer";
|
||||
import { LocalLayer } from "./viewerLocalLayer";
|
||||
import type { FactionSnapshot } from "./contracts";
|
||||
import type {
|
||||
CelestialVisual,
|
||||
CameraMode,
|
||||
ClaimVisual,
|
||||
ConstructionSiteVisual,
|
||||
DragMode,
|
||||
HistoryWindowState,
|
||||
NetworkStats,
|
||||
NodeVisual,
|
||||
OrbitLineVisual,
|
||||
PerformanceStats,
|
||||
Selectable,
|
||||
ShipVisual,
|
||||
StructureVisual,
|
||||
SystemVisual,
|
||||
WorldState,
|
||||
PovLevel,
|
||||
@@ -51,10 +44,10 @@ export class ViewerAppController {
|
||||
private readonly renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
|
||||
// ── Three independent rendering layers ───────────────────────────────────
|
||||
private readonly universeLayer = new UniverseLayer();
|
||||
private readonly galaxyLayer = new GalaxyLayer();
|
||||
private readonly systemLayer = new SystemLayer();
|
||||
private readonly localLayer = new LocalLayer();
|
||||
readonly universeLayer = new UniverseLayer();
|
||||
readonly galaxyLayer = new GalaxyLayer();
|
||||
readonly systemLayer = new SystemLayer();
|
||||
readonly localLayer = new LocalLayer();
|
||||
|
||||
private readonly clock = new THREE.Clock();
|
||||
private readonly raycaster = new THREE.Raycaster();
|
||||
@@ -70,16 +63,6 @@ export class ViewerAppController {
|
||||
|
||||
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 gameSummaryEl: HTMLSpanElement;
|
||||
private readonly systemPanelEl: HTMLDivElement;
|
||||
@@ -98,6 +81,7 @@ export class ViewerAppController {
|
||||
private readonly historyLayerEl: HTMLDivElement;
|
||||
private readonly marqueeEl: HTMLDivElement;
|
||||
private readonly hoverLabelEl: HTMLDivElement;
|
||||
private readonly hoverConnectorLineEl: SVGLineElement;
|
||||
|
||||
private world?: WorldState;
|
||||
private worldTimeSyncMs = performance.now();
|
||||
@@ -165,6 +149,7 @@ export class ViewerAppController {
|
||||
this.historyLayerEl = hud.historyLayerEl;
|
||||
this.marqueeEl = hud.marqueeEl;
|
||||
this.hoverLabelEl = hud.hoverLabelEl;
|
||||
this.hoverConnectorLineEl = hud.hoverConnectorLineEl;
|
||||
({
|
||||
sceneDataController: this.sceneDataController,
|
||||
navigationController: this.navigationController,
|
||||
@@ -231,13 +216,10 @@ export class ViewerAppController {
|
||||
renderFrame({
|
||||
clock: this.clock,
|
||||
renderer: this.renderer,
|
||||
universeScene: this.universeLayer.scene,
|
||||
galaxyScene: this.galaxyLayer.scene,
|
||||
galaxyCamera: this.galaxyLayer.camera,
|
||||
systemScene: this.systemLayer.scene,
|
||||
systemCamera: this.systemLayer.camera,
|
||||
localScene: this.localLayer.scene,
|
||||
localCamera: this.localLayer.camera,
|
||||
universeLayer: this.universeLayer,
|
||||
galaxyLayer: this.galaxyLayer,
|
||||
systemLayer: this.systemLayer,
|
||||
localLayer: this.localLayer,
|
||||
getPovLevel: () => this.povLevel,
|
||||
updateCamera: (delta) => this.updateCamera(delta),
|
||||
updateAmbience: (delta) => this.presentationController.updateAmbience(delta),
|
||||
@@ -294,7 +276,7 @@ export class ViewerAppController {
|
||||
|
||||
// Update star dot scales in galaxy scene
|
||||
updateSystemStarPresentation(
|
||||
this.systemVisuals,
|
||||
this.galaxyLayer.systemVisuals,
|
||||
this.activeSystemId,
|
||||
this.galaxyLayer.camera,
|
||||
(sprite, opacity) => this.setShellReticleOpacity(sprite, opacity),
|
||||
@@ -343,9 +325,9 @@ export class ViewerAppController {
|
||||
private onResize = () => {
|
||||
resizeViewer({
|
||||
renderer: this.renderer,
|
||||
galaxyCamera: this.galaxyLayer.camera,
|
||||
systemCamera: this.systemLayer.camera,
|
||||
localCamera: this.localLayer.camera,
|
||||
galaxyLayer: this.galaxyLayer,
|
||||
systemLayer: this.systemLayer,
|
||||
localLayer: this.localLayer,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -354,7 +336,7 @@ export class ViewerAppController {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -7,6 +7,8 @@ export type {
|
||||
OrbitalSimulationSnapshot,
|
||||
} from "./contractsWorld";
|
||||
export type {
|
||||
StarSnapshot,
|
||||
MoonSnapshot,
|
||||
SystemSnapshot,
|
||||
PlanetSnapshot,
|
||||
ResourceNodeSnapshot,
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
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 {
|
||||
id: string;
|
||||
label: string;
|
||||
galaxyPosition: Vector3Dto;
|
||||
starKind: string;
|
||||
starCount: number;
|
||||
starColor: string;
|
||||
starSize: number;
|
||||
stars: StarSnapshot[];
|
||||
planets: PlanetSnapshot[];
|
||||
}
|
||||
|
||||
@@ -15,7 +33,7 @@ export interface PlanetSnapshot {
|
||||
label: string;
|
||||
planetType: string;
|
||||
shape: string;
|
||||
moonCount: number;
|
||||
moons: MoonSnapshot[];
|
||||
orbitRadius: number;
|
||||
orbitSpeed: number;
|
||||
orbitEccentricity: number;
|
||||
|
||||
@@ -65,6 +65,25 @@ canvas {
|
||||
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 {
|
||||
position: absolute;
|
||||
padding: 8px 10px;
|
||||
|
||||
@@ -215,6 +215,12 @@ export function resolveSelectionPosition(params: ResolveSelectionPositionParams)
|
||||
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);
|
||||
return system ? scaleGalaxyVector(toThreeVector(system.galaxyPosition)) : undefined;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ export const NAV_DISTANCE: Record<PovLevel, number> = {
|
||||
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 GALAXY_PARALLAX_FACTOR = 0.025;
|
||||
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 PLANET_RENDER_SCALE = 0.95;
|
||||
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 interface ZoomBlend {
|
||||
|
||||
@@ -25,15 +25,14 @@ export function createViewerControllers(host: any) {
|
||||
shipGroup: host.systemLayer.shipGroup,
|
||||
galaxySelectableTargets: host.galaxyLayer.selectableTargets,
|
||||
systemSelectableTargets: host.systemLayer.selectableTargets,
|
||||
systemVisuals: host.systemVisuals,
|
||||
planetVisuals: host.planetVisuals,
|
||||
orbitLines: host.orbitLines,
|
||||
celestialVisuals: host.celestialVisuals,
|
||||
nodeVisuals: host.nodeVisuals,
|
||||
stationVisuals: host.stationVisuals,
|
||||
claimVisuals: host.claimVisuals,
|
||||
constructionSiteVisuals: host.constructionSiteVisuals,
|
||||
shipVisuals: host.shipVisuals,
|
||||
systemVisuals: host.galaxyLayer.systemVisuals,
|
||||
planetVisuals: host.systemLayer.planetVisuals,
|
||||
celestialVisuals: host.systemLayer.celestialVisuals,
|
||||
nodeVisuals: host.systemLayer.nodeVisuals,
|
||||
stationVisuals: host.systemLayer.stationVisuals,
|
||||
claimVisuals: host.systemLayer.claimVisuals,
|
||||
constructionSiteVisuals: host.systemLayer.constructionSiteVisuals,
|
||||
shipVisuals: host.systemLayer.shipVisuals,
|
||||
});
|
||||
|
||||
const navigationController = new ViewerNavigationController({
|
||||
@@ -62,10 +61,10 @@ export function createViewerControllers(host: any) {
|
||||
systemAnchor: host.systemAnchor,
|
||||
galaxyCamera: host.galaxyLayer.camera,
|
||||
systemCamera: host.systemLayer.camera,
|
||||
shipVisuals: host.shipVisuals,
|
||||
nodeVisuals: host.nodeVisuals,
|
||||
planetVisuals: host.planetVisuals,
|
||||
systemVisuals: host.systemVisuals,
|
||||
shipVisuals: host.systemLayer.shipVisuals,
|
||||
nodeVisuals: host.systemLayer.nodeVisuals,
|
||||
planetVisuals: host.systemLayer.planetVisuals,
|
||||
systemVisuals: host.galaxyLayer.systemVisuals,
|
||||
followCameraPosition: host.followCameraPosition,
|
||||
followCameraFocus: host.followCameraFocus,
|
||||
followCameraDirection: host.followCameraDirection,
|
||||
@@ -103,9 +102,8 @@ export function createViewerControllers(host: any) {
|
||||
getSelectedItems: () => host.selectedItems,
|
||||
getWorldTimeSyncMs: () => host.worldTimeSyncMs,
|
||||
getCurrentDistance: () => host.currentDistance,
|
||||
planetVisuals: host.planetVisuals,
|
||||
orbitLines: host.orbitLines,
|
||||
systemVisuals: host.systemVisuals,
|
||||
planetVisuals: host.systemLayer.planetVisuals,
|
||||
systemVisuals: host.galaxyLayer.systemVisuals,
|
||||
createWorldPresentationContext: () => host.createWorldPresentationContext(),
|
||||
});
|
||||
|
||||
@@ -198,6 +196,7 @@ export function createViewerControllers(host: any) {
|
||||
galaxySelectableTargets: host.galaxyLayer.selectableTargets,
|
||||
systemSelectableTargets: host.systemLayer.selectableTargets,
|
||||
hoverLabelEl: host.hoverLabelEl,
|
||||
hoverConnectorLineEl: host.hoverConnectorLineEl,
|
||||
marqueeEl: host.marqueeEl,
|
||||
keyState: host.keyState,
|
||||
getWorld: () => host.world,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as THREE from "three";
|
||||
import type { Selectable } from "./viewerTypes";
|
||||
import type { Selectable, SystemVisual } from "./viewerTypes";
|
||||
|
||||
/**
|
||||
* Galaxy rendering layer — the galaxy map.
|
||||
@@ -15,6 +15,7 @@ export class GalaxyLayer {
|
||||
readonly systemGroup = new THREE.Group();
|
||||
|
||||
readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
|
||||
readonly systemVisuals = new Map<string, SystemVisual>();
|
||||
|
||||
constructor() {
|
||||
this.scene.fog = new THREE.FogExp2(0x040912, 0.000035);
|
||||
@@ -34,4 +35,8 @@ export class GalaxyLayer {
|
||||
this.camera.aspect = aspect;
|
||||
this.camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
render(renderer: THREE.WebGLRenderer) {
|
||||
renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface ViewerHudElements {
|
||||
historyLayerEl: HTMLDivElement;
|
||||
marqueeEl: HTMLDivElement;
|
||||
hoverLabelEl: HTMLDivElement;
|
||||
hoverConnectorLineEl: SVGLineElement;
|
||||
}
|
||||
|
||||
export function createViewerHud(documentRef: Document): ViewerHudElements {
|
||||
@@ -73,6 +74,9 @@ export function createViewerHud(documentRef: Document): ViewerHudElements {
|
||||
<div class="history-layer"></div>
|
||||
<section class="ops-strip"></section>
|
||||
<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>
|
||||
`;
|
||||
|
||||
@@ -97,5 +101,6 @@ export function createViewerHud(documentRef: Document): ViewerHudElements {
|
||||
historyLayerEl: root.querySelector(".history-layer") as HTMLDivElement,
|
||||
marqueeEl: root.querySelector(".marquee-box") as HTMLDivElement,
|
||||
hoverLabelEl: root.querySelector(".hover-label") as HTMLDivElement,
|
||||
hoverConnectorLineEl: root.querySelector(".hover-connector-line") as unknown as SVGLineElement,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export function pickSelectableHitAtClientPosition(
|
||||
export function updateHoverLabel(params: {
|
||||
dragMode?: string;
|
||||
hoverLabelEl: HTMLDivElement;
|
||||
hoverConnectorLineEl: SVGLineElement;
|
||||
hoverPick: HoverPickResult | undefined;
|
||||
activeSystemId?: string;
|
||||
povLevel: PovLevel;
|
||||
@@ -77,6 +78,7 @@ export function updateHoverLabel(params: {
|
||||
const {
|
||||
dragMode,
|
||||
hoverLabelEl,
|
||||
hoverConnectorLineEl,
|
||||
hoverPick,
|
||||
activeSystemId,
|
||||
povLevel,
|
||||
@@ -84,13 +86,9 @@ export function updateHoverLabel(params: {
|
||||
point,
|
||||
} = params;
|
||||
|
||||
if (dragMode) {
|
||||
hoverLabelEl.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hoverPick) {
|
||||
if (dragMode || !hoverPick) {
|
||||
hoverLabelEl.hidden = true;
|
||||
hoverConnectorLineEl.setAttribute("hidden", "");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,6 +96,7 @@ export function updateHoverLabel(params: {
|
||||
const label = describeHoverLabel(world, selection);
|
||||
if (!label) {
|
||||
hoverLabelEl.hidden = true;
|
||||
hoverConnectorLineEl.setAttribute("hidden", "");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,8 +104,16 @@ export function updateHoverLabel(params: {
|
||||
|
||||
hoverLabelEl.hidden = false;
|
||||
hoverLabelEl.textContent = `${label}\n${distance}`;
|
||||
hoverLabelEl.style.left = `${point.x + 14}px`;
|
||||
hoverLabelEl.style.top = `${point.y + 14}px`;
|
||||
hoverLabelEl.style.left = `${point.x + 44}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(
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
toggleCameraMode,
|
||||
navigateFromWheel,
|
||||
} from "./viewerControls";
|
||||
import { NAV_DISTANCE, NAV_DISTANCE_PLANET_ORBIT } from "./viewerConstants";
|
||||
import { ViewerHistoryWindowController } from "./viewerHistoryWindowController";
|
||||
import type {
|
||||
CameraMode,
|
||||
@@ -30,6 +31,7 @@ export interface ViewerInteractionContext {
|
||||
galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
hoverLabelEl: HTMLDivElement;
|
||||
hoverConnectorLineEl: SVGLineElement;
|
||||
marqueeEl: HTMLDivElement;
|
||||
keyState: Set<string>;
|
||||
getWorld: () => WorldState | undefined;
|
||||
@@ -231,8 +233,12 @@ export class ViewerInteractionController {
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.focusOnSelection(selectedItems[0]);
|
||||
const selection = selectedItems[0];
|
||||
this.context.focusOnSelection(selection);
|
||||
this.context.syncFollowStateFromSelection();
|
||||
if (selection.kind === "planet") {
|
||||
this.context.setDesiredDistance(NAV_DISTANCE_PLANET_ORBIT);
|
||||
}
|
||||
};
|
||||
|
||||
readonly onWheel = (event: WheelEvent) => {
|
||||
@@ -269,6 +275,7 @@ export class ViewerInteractionController {
|
||||
updateHoverLabel({
|
||||
dragMode: this.context.getDragMode(),
|
||||
hoverLabelEl: this.context.hoverLabelEl,
|
||||
hoverConnectorLineEl: this.context.hoverConnectorLineEl,
|
||||
hoverPick: this.pickSelectableHitAtClientPosition(event.clientX, event.clientY),
|
||||
activeSystemId: this.context.getActiveSystemId(),
|
||||
povLevel: this.context.getPovLevel(),
|
||||
|
||||
@@ -21,4 +21,8 @@ export class LocalLayer {
|
||||
this.camera.aspect = aspect;
|
||||
this.camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
render(renderer: THREE.WebGLRenderer) {
|
||||
renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MOON_RENDER_SCALE } from "./viewerConstants";
|
||||
import type {
|
||||
ShipSnapshot,
|
||||
PlanetSnapshot,
|
||||
MoonSnapshot,
|
||||
Vector3Dto,
|
||||
WorldSnapshot,
|
||||
} from "./contracts";
|
||||
@@ -176,7 +177,7 @@ export function computePlanetLocalPosition(planet: PlanetSnapshot, timeSeconds:
|
||||
const eccentricAnomaly = meanAnomaly
|
||||
+ (eccentricity * Math.sin(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 local = new THREE.Vector3(
|
||||
semiMajorAxis * (Math.cos(eccentricAnomaly) - eccentricity),
|
||||
@@ -190,47 +191,24 @@ export function computePlanetLocalPosition(planet: PlanetSnapshot, timeSeconds:
|
||||
return local;
|
||||
}
|
||||
|
||||
export function computeMoonOrbitRadius(planet: PlanetSnapshot, moonIndex: number, seed: number): number {
|
||||
const spacing = planet.size * 1.4;
|
||||
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);
|
||||
|
||||
export function computeMoonLocalPosition(moon: MoonSnapshot, timeSeconds: number): THREE.Vector3 {
|
||||
const angle = THREE.MathUtils.degToRad(moon.orbitPhaseAtEpoch) + (timeSeconds * moon.orbitSpeed);
|
||||
const local = new THREE.Vector3(
|
||||
Math.cos(angle) * orbitRadius,
|
||||
Math.cos(angle) * moon.orbitRadius,
|
||||
0,
|
||||
Math.sin(angle) * orbitRadius,
|
||||
Math.sin(angle) * moon.orbitRadius,
|
||||
);
|
||||
local.applyAxisAngle(new THREE.Vector3(1, 0, 0), inclination);
|
||||
local.applyAxisAngle(new THREE.Vector3(0, 1, 0), node);
|
||||
local.applyAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(moon.orbitInclination));
|
||||
local.applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(moon.orbitLongitudeOfAscendingNode));
|
||||
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 {
|
||||
return Math.max(minRadius, Math.pow(Math.max(size, 0.1), exponent) * scale);
|
||||
}
|
||||
|
||||
export function computeMoonRenderRadius(planet: PlanetSnapshot, moonIndex: number, seed: number): number {
|
||||
return celestialRenderRadius(computeMoonSize(planet, moonIndex, seed), 0.00011, 0.025, 0.62);
|
||||
export function computeMoonRenderRadius(moon: MoonSnapshot): number {
|
||||
return celestialRenderRadius(moon.size, 0.00011, 0.025, 0.62);
|
||||
}
|
||||
|
||||
export function starHaloOpacity(starKind: string): number {
|
||||
@@ -251,7 +229,6 @@ export function resolveOrbitalAnchorPosition(
|
||||
systemId: string,
|
||||
anchor: OrbitalAnchor,
|
||||
timeSeconds: number,
|
||||
seed: number,
|
||||
): THREE.Vector3 {
|
||||
if (!world || anchor.kind === "star") {
|
||||
return new THREE.Vector3();
|
||||
@@ -268,5 +245,6 @@ export function resolveOrbitalAnchorPosition(
|
||||
return planetPosition;
|
||||
}
|
||||
|
||||
return planetPosition.add(computeMoonLocalPosition(planet, anchor.moonIndex, timeSeconds, seed));
|
||||
const moon = planet.moons[anchor.moonIndex];
|
||||
return planetPosition.add(computeMoonLocalPosition(moon, timeSeconds));
|
||||
}
|
||||
|
||||
@@ -351,13 +351,27 @@ export function updateDetailPanel(
|
||||
detailBodyEl.innerHTML = `
|
||||
<p>${system.label}</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>Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°</p>
|
||||
`;
|
||||
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);
|
||||
if (!system) {
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,14 @@ import * as THREE from "three";
|
||||
import { ACTIVE_SYSTEM_DETAIL_SCALE, PROJECTED_GALAXY_RADIUS } from "./viewerConstants";
|
||||
import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeSeconds, scaleLocalVector } from "./viewerMath";
|
||||
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()) {
|
||||
const elapsedMs = now - visual.receivedAtMs;
|
||||
@@ -26,6 +34,7 @@ export function updatePlanetPresentation(
|
||||
world: WorldState | undefined,
|
||||
worldTimeSyncMs: number,
|
||||
planetVisuals: PlanetVisual[],
|
||||
systemCamera: THREE.PerspectiveCamera,
|
||||
) {
|
||||
const nowSeconds = currentWorldTimeSeconds(world, worldTimeSyncMs);
|
||||
// In systemScene all positions use scaleLocalVector * ACTIVE_SYSTEM_DETAIL_SCALE.
|
||||
@@ -34,23 +43,44 @@ export function updatePlanetPresentation(
|
||||
const position = scaleLocalVector(computePlanetLocalPosition(visual.planet, nowSeconds))
|
||||
.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.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) {
|
||||
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()) {
|
||||
moon.orbit.setPosition(position);
|
||||
moon.orbit.setScaleScalar(ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||
moon.mesh.setPosition(
|
||||
position.clone().add(
|
||||
scaleLocalVector(computeMoonLocalPosition(visual.planet, moonIndex, nowSeconds, world?.seed ?? 1))
|
||||
.multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE),
|
||||
),
|
||||
const moonPos = position.clone().add(
|
||||
scaleLocalVector(computeMoonLocalPosition(visual.planet.moons[moonIndex], nowSeconds))
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
import { updatePlanetPresentation } from "./viewerPresentation";
|
||||
import { renderRecentEvents, updateGameStatus, updateSystemSummaries, updateWorldPresentation } from "./viewerWorldPresentation";
|
||||
import { updateSystemPanel } from "./viewerPanels";
|
||||
import { createBackdropStars, createNebulaClouds, createNebulaTexture } from "./viewerSceneFactory";
|
||||
import type { OrbitLineVisual, Selectable } from "./viewerTypes";
|
||||
import { createBackdropStars, createMilkyWayBand, createNebulaClouds, createNebulaTexture } from "./viewerSceneFactory";
|
||||
import type { Selectable } from "./viewerTypes";
|
||||
|
||||
export interface ViewerPresentationContext {
|
||||
renderer: THREE.WebGLRenderer;
|
||||
@@ -40,7 +40,6 @@ export interface ViewerPresentationContext {
|
||||
getWorldTimeSyncMs: () => number;
|
||||
getCurrentDistance: () => number;
|
||||
planetVisuals: any[];
|
||||
orbitLines: OrbitLineVisual[];
|
||||
systemVisuals: Map<any, any>;
|
||||
createWorldPresentationContext: () => any;
|
||||
}
|
||||
@@ -50,30 +49,28 @@ export class ViewerPresentationController {
|
||||
|
||||
initializeAmbience() {
|
||||
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(createMilkyWayBand(document));
|
||||
}
|
||||
|
||||
updateAmbience(delta: number) {
|
||||
updateAmbience(_delta: number) {
|
||||
const activeCamera = this.context.getPovLevel() === "galaxy"
|
||||
? this.context.galaxyCamera
|
||||
: this.context.systemCamera;
|
||||
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() {
|
||||
const activeSystemId = this.context.getActiveSystemId();
|
||||
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);
|
||||
|
||||
const showPlanetIcons = povLevel !== "local";
|
||||
for (const visual of this.context.planetVisuals) {
|
||||
visual.icon.setVisible(showPlanetIcons);
|
||||
}
|
||||
}
|
||||
|
||||
updateNetworkPanel() {
|
||||
@@ -100,6 +97,7 @@ export class ViewerPresentationController {
|
||||
world,
|
||||
this.context.getWorldTimeSyncMs(),
|
||||
this.context.planetVisuals,
|
||||
this.context.systemCamera,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,21 +146,4 @@ export class ViewerPresentationController {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import * as THREE from "three";
|
||||
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 {
|
||||
clock: THREE.Clock;
|
||||
renderer: THREE.WebGLRenderer;
|
||||
universeScene: THREE.Scene;
|
||||
galaxyScene: THREE.Scene;
|
||||
galaxyCamera: THREE.PerspectiveCamera;
|
||||
systemScene: THREE.Scene;
|
||||
systemCamera: THREE.PerspectiveCamera;
|
||||
localScene: THREE.Scene;
|
||||
localCamera: THREE.PerspectiveCamera;
|
||||
universeLayer: UniverseLayer;
|
||||
galaxyLayer: GalaxyLayer;
|
||||
systemLayer: SystemLayer;
|
||||
localLayer: LocalLayer;
|
||||
getPovLevel: () => PovLevel;
|
||||
updateCamera: (delta: number) => void;
|
||||
updateAmbience: (delta: number) => void;
|
||||
@@ -25,9 +26,9 @@ export interface RenderFrameParams {
|
||||
|
||||
export interface ResizeParams {
|
||||
renderer: THREE.WebGLRenderer;
|
||||
galaxyCamera: THREE.PerspectiveCamera;
|
||||
systemCamera: THREE.PerspectiveCamera;
|
||||
localCamera: THREE.PerspectiveCamera;
|
||||
galaxyLayer: GalaxyLayer;
|
||||
systemLayer: SystemLayer;
|
||||
localLayer: LocalLayer;
|
||||
}
|
||||
|
||||
export interface CameraStepParams {
|
||||
@@ -48,22 +49,22 @@ export function renderFrame(params: RenderFrameParams) {
|
||||
params.applyZoomPresentation();
|
||||
|
||||
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.clear();
|
||||
// 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();
|
||||
if (povLevel === "galaxy") {
|
||||
// Galaxy map on top of universe backdrop
|
||||
params.renderer.render(params.galaxyScene, params.galaxyCamera);
|
||||
params.galaxyLayer.render(params.renderer);
|
||||
} else if (povLevel === "system") {
|
||||
params.renderer.render(params.systemScene, params.systemCamera);
|
||||
params.systemLayer.render(params.renderer);
|
||||
} else {
|
||||
// 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.render(params.localScene, params.localCamera);
|
||||
params.localLayer.render(params.renderer);
|
||||
}
|
||||
|
||||
params.recordPerformanceStats(performance.now() - frameStartedAtMs);
|
||||
@@ -73,10 +74,9 @@ export function renderFrame(params: RenderFrameParams) {
|
||||
export function resizeViewer(params: ResizeParams) {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
for (const camera of [params.galaxyCamera, params.systemCamera, params.localCamera]) {
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
params.galaxyLayer.onResize(width / height);
|
||||
params.systemLayer.onResize(width / height);
|
||||
params.localLayer.onResize(width / height);
|
||||
params.renderer.setSize(width, height);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import type {
|
||||
StationSnapshot,
|
||||
SystemSnapshot,
|
||||
} from "./contracts";
|
||||
import type { OrbitLineVisual, OrbitalAnchor, Selectable } from "./viewerTypes";
|
||||
import type { OrbitalAnchor, Selectable } from "./viewerTypes";
|
||||
import { rawObject } from "./viewerScenePrimitives";
|
||||
|
||||
export interface ViewerSceneDataContext {
|
||||
@@ -65,7 +65,6 @@ export interface ViewerSceneDataContext {
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
systemVisuals: Map<any, any>;
|
||||
planetVisuals: any[];
|
||||
orbitLines: OrbitLineVisual[];
|
||||
celestialVisuals: Map<any, any>;
|
||||
nodeVisuals: Map<any, any>;
|
||||
stationVisuals: Map<any, any>;
|
||||
@@ -251,7 +250,6 @@ export class ViewerSceneDataController {
|
||||
systemSelectableTargets: this.context.systemSelectableTargets,
|
||||
systemVisuals: this.context.systemVisuals,
|
||||
planetVisuals: this.context.planetVisuals,
|
||||
orbitLines: this.context.orbitLines,
|
||||
celestialVisuals: this.context.celestialVisuals,
|
||||
nodeVisuals: this.context.nodeVisuals,
|
||||
stationVisuals: this.context.stationVisuals,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
ACTIVE_SYSTEM_DETAIL_SCALE,
|
||||
MOON_RENDER_SCALE,
|
||||
PLANET_RENDER_SCALE,
|
||||
STAR_RENDER_SCALE,
|
||||
@@ -8,6 +9,7 @@ import type {
|
||||
CelestialSnapshot,
|
||||
ClaimSnapshot,
|
||||
ConstructionSiteSnapshot,
|
||||
MoonSnapshot,
|
||||
PlanetSnapshot,
|
||||
ResourceNodeSnapshot,
|
||||
ShipSnapshot,
|
||||
@@ -17,10 +19,9 @@ import type {
|
||||
import type { MoonVisual } from "./viewerTypes";
|
||||
import {
|
||||
celestialRenderRadius,
|
||||
computeMoonOrbitRadius,
|
||||
computeMoonLocalPosition,
|
||||
computeMoonRenderRadius,
|
||||
computePlanetLocalPosition,
|
||||
scaleLocalScalar,
|
||||
scaleLocalVector,
|
||||
starHaloOpacity,
|
||||
toThreeVector,
|
||||
@@ -84,45 +85,34 @@ export function createConstructionSiteMesh(site: ConstructionSiteSnapshot): Scen
|
||||
|
||||
export function createStarCluster(system: SystemSnapshot): SceneNode {
|
||||
const root = new THREE.Group();
|
||||
const renderedStarSize = celestialRenderRadius(system.starSize, 0.00018, 0.16, 0.62);
|
||||
const offsets = system.starCount > 1
|
||||
? [new THREE.Vector3(-renderedStarSize * 0.55, 0, 0), new THREE.Vector3(renderedStarSize * 0.75, renderedStarSize * 0.08, 0)]
|
||||
: [new THREE.Vector3(0, 0, 0)];
|
||||
|
||||
for (const [index, offset] of offsets.entries()) {
|
||||
const sizeScale = index === 0 ? 1 : 0.72;
|
||||
const star = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(renderedStarSize * sizeScale, 24, 24),
|
||||
new THREE.MeshBasicMaterial({ color: system.starColor }),
|
||||
for (const [index, star] of system.stars.entries()) {
|
||||
const renderedSize = celestialRenderRadius(star.size, 0.00018, 40, 0.62);
|
||||
const offset = system.stars.length > 1
|
||||
? (index === 0
|
||||
? new THREE.Vector3(-renderedSize * 0.55, 0, 0)
|
||||
: new THREE.Vector3(renderedSize * 0.75, renderedSize * 0.08, 0))
|
||||
: new THREE.Vector3(0, 0, 0);
|
||||
const mesh = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(renderedSize, 24, 24),
|
||||
new THREE.MeshBasicMaterial({ color: star.color }),
|
||||
);
|
||||
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({
|
||||
color: system.starColor,
|
||||
color: star.color,
|
||||
transparent: true,
|
||||
opacity: starHaloOpacity(system.starKind),
|
||||
opacity: starHaloOpacity(star.kind),
|
||||
side: THREE.BackSide,
|
||||
}),
|
||||
);
|
||||
star.position.copy(offset);
|
||||
mesh.position.copy(offset);
|
||||
halo.position.copy(offset);
|
||||
root.add(star, halo);
|
||||
root.add(mesh, halo);
|
||||
}
|
||||
|
||||
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 {
|
||||
const renderedPlanetRadius = celestialRenderRadius(planet.size, 0.00012, 0.03, 0.62);
|
||||
@@ -140,41 +130,74 @@ export function createPlanetRing(planet: PlanetSnapshot): SceneNode {
|
||||
return createSceneNode(ring);
|
||||
}
|
||||
|
||||
export function createMoonVisuals(planet: PlanetSnapshot, seed: number): MoonVisual[] {
|
||||
const moonCount = Math.min(planet.moonCount, 12);
|
||||
const moons: MoonVisual[] = [];
|
||||
|
||||
for (let moonIndex = 0; moonIndex < moonCount; moonIndex += 1) {
|
||||
const orbitRadius = scaleLocalScalar(computeMoonOrbitRadius(planet, moonIndex, seed));
|
||||
const orbit = new THREE.LineLoop(
|
||||
new THREE.BufferGeometry().setFromPoints(
|
||||
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 }),
|
||||
function createMoonOrbit(moon: MoonSnapshot): SceneNode {
|
||||
const segments = 64;
|
||||
const period = (2 * Math.PI) / Math.max(Math.abs(moon.orbitSpeed), 1e-6);
|
||||
const points: THREE.Vector3[] = [];
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
points.push(
|
||||
scaleLocalVector(computeMoonLocalPosition(moon, (i / segments) * period))
|
||||
.multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE),
|
||||
);
|
||||
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(
|
||||
new THREE.SphereGeometry(moonSize, 12, 12),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(planet.color).lerp(new THREE.Color("#d9dee7"), 0.55),
|
||||
color: moon.color,
|
||||
roughness: 0.96,
|
||||
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 moons;
|
||||
return createSceneNode(new THREE.LineLoop(
|
||||
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 {
|
||||
@@ -201,32 +224,160 @@ export function createShipMesh(ship: ShipSnapshot, size: number, length: number,
|
||||
return createSceneNode(mesh);
|
||||
}
|
||||
|
||||
export function createBackdropStars(): THREE.Points {
|
||||
const starCount = 1800;
|
||||
const radius = 36000;
|
||||
function createStarGlowTexture(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 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 colors = new Float32Array(starCount * 3);
|
||||
const color = new THREE.Color();
|
||||
|
||||
for (let index = 0; index < starCount; 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.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 + 1] = direction.y;
|
||||
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 + 1] = color.g;
|
||||
colors[index * 3 + 2] = color.b;
|
||||
@@ -239,77 +390,244 @@ export function createBackdropStars(): THREE.Points {
|
||||
return new THREE.Points(
|
||||
geometry,
|
||||
new THREE.PointsMaterial({
|
||||
size: 2.2,
|
||||
size,
|
||||
sizeAttenuation: false,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
opacity,
|
||||
depthWrite: false,
|
||||
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 {
|
||||
const canvas = documentRef.createElement("canvas");
|
||||
canvas.width = 256;
|
||||
canvas.height = 256;
|
||||
canvas.width = 512;
|
||||
canvas.height = 512;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
throw new Error("Unable to create nebula texture");
|
||||
}
|
||||
|
||||
const gradient = context.createRadialGradient(128, 128, 18, 128, 128, 118);
|
||||
gradient.addColorStop(0, "rgba(255,255,255,0.95)");
|
||||
gradient.addColorStop(0.2, "rgba(255,255,255,0.48)");
|
||||
gradient.addColorStop(0.55, "rgba(140,180,255,0.14)");
|
||||
gradient.addColorStop(1, "rgba(0,0,0,0)");
|
||||
context.fillStyle = gradient;
|
||||
context.fillRect(0, 0, 256, 256);
|
||||
const palettes = [
|
||||
["rgba(80,220,255,0.24)", "rgba(120,110,255,0.18)", "rgba(255,255,255,0.14)"],
|
||||
["rgba(255,130,205,0.24)", "rgba(110,170,255,0.16)", "rgba(255,240,255,0.12)"],
|
||||
["rgba(120,255,205,0.2)", "rgba(100,160,255,0.18)", "rgba(255,255,255,0.1)"],
|
||||
];
|
||||
|
||||
for (let index = 0; index < 10; index += 1) {
|
||||
const x = THREE.MathUtils.randFloat(30, 226);
|
||||
const y = THREE.MathUtils.randFloat(30, 226);
|
||||
const radius = THREE.MathUtils.randFloat(24, 72);
|
||||
const puff = context.createRadialGradient(x, y, 0, x, y, radius);
|
||||
puff.addColorStop(0, "rgba(255,255,255,0.16)");
|
||||
puff.addColorStop(0.45, "rgba(255,255,255,0.08)");
|
||||
puff.addColorStop(1, "rgba(0,0,0,0)");
|
||||
context.fillStyle = puff;
|
||||
context.clearRect(0, 0, 512, 512);
|
||||
|
||||
for (let layer = 0; layer < palettes.length; layer += 1) {
|
||||
for (let index = 0; index < 18; index += 1) {
|
||||
const x = THREE.MathUtils.randFloat(40, 472);
|
||||
const y = THREE.MathUtils.randFloat(40, 472);
|
||||
const radius = THREE.MathUtils.randFloat(55, 180);
|
||||
const [core, mid, edge] = palettes[layer];
|
||||
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.arc(x, y, radius, 0, Math.PI * 2);
|
||||
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);
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
export function createNebulaClouds(texture: THREE.Texture): THREE.Sprite[] {
|
||||
const directions = [
|
||||
new THREE.Vector3(0.74, 0.34, -0.58),
|
||||
new THREE.Vector3(-0.62, 0.18, -0.77),
|
||||
new THREE.Vector3(0.22, -0.44, -0.87),
|
||||
new THREE.Vector3(-0.38, 0.56, 0.73),
|
||||
const seeds = [
|
||||
{ direction: new THREE.Vector3(0.76, 0.28, -0.58), color: "#5bd4ff", scale: 24000, opacity: 0.22, rotation: 0.18 },
|
||||
{ direction: new THREE.Vector3(0.7, 0.34, -0.54), color: "#93b3ff", scale: 18000, opacity: 0.16, rotation: -0.22 },
|
||||
{ direction: new THREE.Vector3(-0.58, 0.24, -0.78), color: "#ff8cc6", scale: 22000, opacity: 0.2, rotation: 0.34 },
|
||||
{ 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({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: 0.14,
|
||||
opacity: seed.opacity,
|
||||
depthWrite: false,
|
||||
color: ["#6dc7ff", "#ff9ec8", "#8e7dff", "#7ce0c3"][index] ?? "#6dc7ff",
|
||||
color: seed.color,
|
||||
blending: THREE.AdditiveBlending,
|
||||
fog: false,
|
||||
}));
|
||||
sprite.position.copy(direction.normalize().multiplyScalar(25000 + index * 2600));
|
||||
const scale = 15000 + index * 2400;
|
||||
sprite.scale.set(scale, scale * 0.62, 1);
|
||||
sprite.position.copy(seed.direction.normalize().multiplyScalar(23000 + index * 1800));
|
||||
sprite.material.rotation = seed.rotation;
|
||||
sprite.scale.set(seed.scale, seed.scale * THREE.MathUtils.randFloat(0.52, 0.78), 1);
|
||||
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 {
|
||||
const canvas = documentRef.createElement("canvas");
|
||||
canvas.width = 64;
|
||||
@@ -325,12 +643,6 @@ export function createTacticalIcon(documentRef: Document, color: string, size: n
|
||||
context.beginPath();
|
||||
context.arc(32, 32, 18, 0, Math.PI * 2);
|
||||
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 sprite = new THREE.Sprite(new THREE.SpriteMaterial({
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
ClaimVisual,
|
||||
ConstructionSiteVisual,
|
||||
NodeVisual,
|
||||
OrbitLineVisual,
|
||||
PlanetVisual,
|
||||
Selectable,
|
||||
ShipVisual,
|
||||
@@ -48,6 +47,7 @@ import {
|
||||
createNodeMesh,
|
||||
createPlanetOrbit,
|
||||
createPlanetRing,
|
||||
createPlanetTexture,
|
||||
createShellReticle,
|
||||
createShipMesh,
|
||||
createCelestialMesh,
|
||||
@@ -86,7 +86,6 @@ interface SceneSyncContext {
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
systemVisuals: Map<string, SystemVisual>;
|
||||
planetVisuals: PlanetVisual[];
|
||||
orbitLines: OrbitLineVisual[];
|
||||
celestialVisuals: Map<string, CelestialVisual>;
|
||||
nodeVisuals: Map<string, NodeVisual>;
|
||||
stationVisuals: Map<string, StructureVisual>;
|
||||
@@ -121,7 +120,6 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
||||
context.galaxySelectableTargets.clear();
|
||||
context.systemSelectableTargets.clear();
|
||||
context.planetVisuals.length = 0;
|
||||
context.orbitLines.length = 0;
|
||||
context.systemVisuals.clear();
|
||||
|
||||
for (const system of systems) {
|
||||
@@ -129,7 +127,7 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
||||
const galaxyRoot = createSceneNode(new THREE.Group());
|
||||
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);
|
||||
galaxyRoot.add(systemIcon, shellReticle);
|
||||
|
||||
@@ -150,50 +148,45 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
||||
);
|
||||
|
||||
for (const [planetIndex, planet] of system.planets.entries()) {
|
||||
const orbit = createPlanetOrbit(planet);
|
||||
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(
|
||||
new THREE.SphereGeometry(renderedPlanetRadius, 18, 18),
|
||||
new THREE.SphereGeometry(renderedPlanetRadius, 24, 24),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: planet.color,
|
||||
roughness: 0.92,
|
||||
metalness: 0.08,
|
||||
emissive: new THREE.Color(planet.color).multiplyScalar(0.04),
|
||||
map: planetTexture,
|
||||
roughness: 0.88,
|
||||
metalness: 0.04,
|
||||
}),
|
||||
));
|
||||
const initialPos = toSystemPos(computePlanetLocalPosition(planet, worldTimeSeconds));
|
||||
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.setVisible(true);
|
||||
const orbit = createPlanetOrbit(planet);
|
||||
const ring = planet.hasRing ? createPlanetRing(planet) : undefined;
|
||||
if (ring) {
|
||||
ring.setPosition(initialPos);
|
||||
}
|
||||
const moons = createMoonVisuals(planet, context.worldSeed);
|
||||
const moons = createMoonVisuals(planet, context.documentRef);
|
||||
detailGroup.add(orbit, planetMesh, planetIcon);
|
||||
if (ring) {
|
||||
detailGroup.add(ring);
|
||||
}
|
||||
for (const moon of moons) {
|
||||
for (const [moonIdx, moon] of moons.entries()) {
|
||||
moon.systemId = system.id;
|
||||
moon.planetIndex = planetIndex;
|
||||
moon.orbit.setPosition(initialPos);
|
||||
moon.mesh.setPosition(initialPos);
|
||||
detailGroup.add(moon.orbit, moon.mesh);
|
||||
context.orbitLines.push({
|
||||
line: moon.orbit,
|
||||
systemId: system.id,
|
||||
kind: "moon",
|
||||
planetIndex,
|
||||
});
|
||||
moon.icon.setPosition(initialPos);
|
||||
detailGroup.add(moon.mesh, moon.icon, moon.orbit);
|
||||
registerSelectableTarget(context.systemSelectableTargets, moon.mesh, { kind: "moon", systemId: system.id, planetIndex, moonIndex: moonIdx });
|
||||
registerSelectableTarget(context.systemSelectableTargets, moon.icon, { kind: "moon", systemId: system.id, planetIndex, moonIndex: moonIdx });
|
||||
}
|
||||
context.orbitLines.push({
|
||||
line: orbit,
|
||||
systemId: system.id,
|
||||
kind: "planet",
|
||||
planetIndex,
|
||||
});
|
||||
context.planetVisuals.push({ systemId: system.id, planet, orbit, mesh: planetMesh, icon: planetIcon, ring, moons });
|
||||
context.planetVisuals.push({ systemId: system.id, planet, orbit, mesh: planetMesh, icon: planetIcon, iconBaseScale, ring, moons });
|
||||
registerSelectableTarget(context.systemSelectableTargets, planetMesh, { 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 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));
|
||||
mesh.setPosition(orbitalAnchor);
|
||||
icon.setPosition(orbitalAnchor);
|
||||
@@ -237,6 +231,7 @@ export function syncCelestials(context: SceneSyncContext, celestials: CelestialS
|
||||
systemId: celestial.systemId,
|
||||
mesh,
|
||||
icon,
|
||||
iconBaseScale: celestialIconBaseScale,
|
||||
kind: celestial.kind,
|
||||
orbitalAnchor,
|
||||
});
|
||||
@@ -252,7 +247,7 @@ export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot
|
||||
|
||||
for (const node of nodes) {
|
||||
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 displayPos = toSystemPos(localPosition);
|
||||
mesh.setPosition(displayPos);
|
||||
@@ -285,7 +280,7 @@ export function syncStations(context: SceneSyncContext, stations: StationSnapsho
|
||||
|
||||
for (const station of stations) {
|
||||
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 displayPos = toSystemPos(localPosition);
|
||||
mesh.setPosition(displayPos);
|
||||
@@ -320,7 +315,7 @@ export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[], a
|
||||
const localPosition = context.resolvePointPosition(claim.systemId, claim.celestialId);
|
||||
const displayPos = toSystemPos(localPosition);
|
||||
const mesh = createClaimMesh(claim);
|
||||
const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 18);
|
||||
const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 90);
|
||||
mesh.setPosition(displayPos);
|
||||
icon.setPosition(displayPos);
|
||||
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 displayPos = toSystemPos(localPosition);
|
||||
const mesh = createConstructionSiteMesh(site);
|
||||
const icon = createTacticalIcon(context.documentRef, "#9df29c", 18);
|
||||
const icon = createTacticalIcon(context.documentRef, "#9df29c", 90);
|
||||
mesh.setPosition(displayPos);
|
||||
icon.setPosition(displayPos);
|
||||
const isActive = site.systemId === activeSystemId;
|
||||
@@ -375,7 +370,7 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
|
||||
for (const ship of ships) {
|
||||
const mesh = createShipMesh(ship, context.shipSize(ship), context.shipLength(ship), 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 displayPos = toSystemPos(localPosition);
|
||||
mesh.setPosition(displayPos);
|
||||
|
||||
@@ -33,6 +33,10 @@ export function describeSelectable(world: WorldState | undefined, item: Selectab
|
||||
if (item.kind === "planet") {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -54,7 +58,7 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
|
||||
if (!system) {
|
||||
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 shipCount = [...world.ships.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}`;
|
||||
}
|
||||
|
||||
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") {
|
||||
const node = world.nodes.get(item.id);
|
||||
if (!node) {
|
||||
@@ -168,6 +182,9 @@ export function resolveSelectableSystemId(world: WorldState | undefined, selecti
|
||||
if (selection.kind === "planet") {
|
||||
return selection.systemId;
|
||||
}
|
||||
if (selection.kind === "moon") {
|
||||
return selection.systemId;
|
||||
}
|
||||
return selection.id;
|
||||
}
|
||||
|
||||
@@ -271,7 +288,7 @@ export function renderSystemDetails(
|
||||
}
|
||||
}
|
||||
for (const planet of system.planets) {
|
||||
moonCount += planet.moonCount;
|
||||
moonCount += planet.moons.length;
|
||||
}
|
||||
|
||||
const followText = activeContext && cameraMode === "follow" && cameraTargetShipId
|
||||
@@ -280,7 +297,7 @@ export function renderSystemDetails(
|
||||
|
||||
return `
|
||||
<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>Celestials ${celestialCount}<br>Resource nodes ${nodeCount}</p>
|
||||
<p>Claims ${claimCount}<br>Construction sites ${constructionCount}</p>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
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.
|
||||
@@ -9,7 +18,7 @@ import type { Selectable } from "./viewerTypes";
|
||||
*/
|
||||
export class SystemLayer {
|
||||
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 nodeGroup = new THREE.Group();
|
||||
@@ -20,6 +29,14 @@ export class SystemLayer {
|
||||
|
||||
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() {
|
||||
this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.55));
|
||||
const keyLight = new THREE.DirectionalLight(0xdcecff, 1.3);
|
||||
@@ -44,4 +61,8 @@ export class SystemLayer {
|
||||
this.camera.aspect = aspect;
|
||||
this.camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
render(renderer: THREE.WebGLRenderer) {
|
||||
renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ export type Selectable =
|
||||
| { kind: "claim"; id: string }
|
||||
| { kind: "construction-site"; 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 {
|
||||
systemId: string;
|
||||
@@ -49,6 +50,7 @@ export interface PlanetVisual {
|
||||
orbit: SceneNode;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
iconBaseScale: number;
|
||||
ring?: SceneNode;
|
||||
moons: MoonVisual[];
|
||||
}
|
||||
@@ -56,17 +58,13 @@ export interface PlanetVisual {
|
||||
export interface MoonVisual {
|
||||
systemId: string;
|
||||
planetIndex: number;
|
||||
moonIndex: number;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
iconBaseScale: number;
|
||||
orbit: SceneNode;
|
||||
}
|
||||
|
||||
export interface OrbitLineVisual {
|
||||
line: SceneNode;
|
||||
systemId: string;
|
||||
kind: "planet" | "moon";
|
||||
planetIndex: number;
|
||||
}
|
||||
|
||||
export type OrbitalAnchor =
|
||||
| { kind: "star" }
|
||||
| { kind: "planet"; planetIndex: number }
|
||||
@@ -89,6 +87,7 @@ export interface CelestialVisual {
|
||||
systemId: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
iconBaseScale: number;
|
||||
kind: string;
|
||||
orbitalAnchor: THREE.Vector3;
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@ export class UniverseLayer {
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
DISPLAY_UNITS_PER_LIGHT_YEAR,
|
||||
KILOMETERS_PER_AU,
|
||||
computeMoonLocalPosition,
|
||||
computeMoonSize,
|
||||
computePlanetLocalPosition,
|
||||
currentWorldTimeSeconds,
|
||||
resolveOrbitalAnchorPosition,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
resolveShipHeading,
|
||||
updateSystemStarPresentation,
|
||||
getAnimatedShipLocalPosition,
|
||||
iconWorldScale,
|
||||
} from "./viewerPresentation";
|
||||
import { rawObject } from "./viewerScenePrimitives";
|
||||
import type {
|
||||
@@ -114,7 +114,15 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
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()) {
|
||||
@@ -351,13 +359,12 @@ export function resolveOrbitalAnchor(context: WorldOrbitalContext, systemId: str
|
||||
bestAnchor = { kind: "planet", planetIndex };
|
||||
}
|
||||
|
||||
const moonCount = Math.min(planet.moonCount, 12);
|
||||
for (let moonIndex = 0; moonIndex < moonCount; moonIndex += 1) {
|
||||
for (const [moonIndex, moon] of planet.moons.entries()) {
|
||||
const moonPosition = planetPosition
|
||||
.clone()
|
||||
.add(computeMoonLocalPosition(planet, moonIndex, nowSeconds, context.world.seed));
|
||||
.add(computeMoonLocalPosition(moon, nowSeconds));
|
||||
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) {
|
||||
bestDistance = moonDistance;
|
||||
bestAnchor = { kind: "moon", planetIndex, moonIndex };
|
||||
@@ -417,9 +424,15 @@ export function computeCelestialLocalPositionById(
|
||||
|
||||
const parentInitialPosition = toThreeVector(parentCelestial.orbitalAnchor);
|
||||
const relativeOffset = basePosition.clone().sub(parentInitialPosition);
|
||||
const initialAngle = Math.atan2(parentInitialPosition.z, parentInitialPosition.x);
|
||||
const currentAngle = Math.atan2(parentCurrentPosition.z, parentCurrentPosition.x);
|
||||
const rotatedOffset = relativeOffset.applyAxisAngle(new THREE.Vector3(0, 1, 0), currentAngle - initialAngle);
|
||||
const initialDir = parentInitialPosition.clone().normalize();
|
||||
const currentDir = parentCurrentPosition.clone().normalize();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -486,7 +499,7 @@ function computeStructureLocalPosition(
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user