feat: migrate simulation to physically-based unit system
Replace arbitrary game units with real-world measurements throughout the simulation and viewer: planet orbits in AU, sizes in km, galaxy positions in light-years. Add SimulationUnits helpers for conversions, separate WarpSpeed from FtlSpeed for ships, fix FTL transit progress to use galaxy-space distances, overhaul Lagrange point placement with Hill sphere approximation, and update the viewer to scale and format all distances correctly. Ships in FTL transit now render in galaxy view. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ public sealed record ShipSnapshot(
|
|||||||
string? CargoItemId,
|
string? CargoItemId,
|
||||||
float WorkerPopulation,
|
float WorkerPopulation,
|
||||||
float EnergyStored,
|
float EnergyStored,
|
||||||
|
float TravelSpeed,
|
||||||
|
string TravelSpeedUnit,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId,
|
string FactionId,
|
||||||
float Health,
|
float Health,
|
||||||
@@ -51,6 +53,8 @@ public sealed record ShipDelta(
|
|||||||
string? CargoItemId,
|
string? CargoItemId,
|
||||||
float WorkerPopulation,
|
float WorkerPopulation,
|
||||||
float EnergyStored,
|
float EnergyStored,
|
||||||
|
float TravelSpeed,
|
||||||
|
string TravelSpeedUnit,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId,
|
string FactionId,
|
||||||
float Health,
|
float Health,
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ public sealed class ShipDefinition
|
|||||||
public required string Role { get; set; }
|
public required string Role { get; set; }
|
||||||
public required string ShipClass { get; set; }
|
public required string ShipClass { get; set; }
|
||||||
public float Speed { get; set; }
|
public float Speed { get; set; }
|
||||||
|
public float WarpSpeed { get; set; }
|
||||||
public float FtlSpeed { get; set; }
|
public float FtlSpeed { get; set; }
|
||||||
public float SpoolTime { get; set; }
|
public float SpoolTime { get; set; }
|
||||||
public float CargoCapacity { get; set; }
|
public float CargoCapacity { get; set; }
|
||||||
|
|||||||
15
apps/backend/Simulation/Model/SimulationUnits.cs
Normal file
15
apps/backend/Simulation/Model/SimulationUnits.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace SpaceGame.Simulation.Api.Simulation;
|
||||||
|
|
||||||
|
public static class SimulationUnits
|
||||||
|
{
|
||||||
|
public const float KilometersPerAu = 149_597_870.7f;
|
||||||
|
public const float MetersPerKilometer = 1000f;
|
||||||
|
|
||||||
|
public static float AuToKilometers(float au) => au * KilometersPerAu;
|
||||||
|
|
||||||
|
public static float AuPerSecondToKilometersPerSecond(float auPerSecond) =>
|
||||||
|
auPerSecond * KilometersPerAu;
|
||||||
|
|
||||||
|
public static float MetersPerSecondToKilometersPerSecond(float metersPerSecond) =>
|
||||||
|
metersPerSecond / MetersPerKilometer;
|
||||||
|
}
|
||||||
@@ -115,9 +115,9 @@ public sealed partial class ScenarioLoader
|
|||||||
var compactPositions = new[]
|
var compactPositions = new[]
|
||||||
{
|
{
|
||||||
new[] { 0f, 0f, 0f },
|
new[] { 0f, 0f, 0f },
|
||||||
new[] { 2600f, 24f, -420f },
|
new[] { 2.6f, 0.02f, -0.42f },
|
||||||
new[] { -2400f, -36f, 560f },
|
new[] { -2.4f, -0.04f, 0.56f },
|
||||||
new[] { 520f, 42f, 2480f },
|
new[] { 0.52f, 0.04f, 2.48f },
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var index = 0; index < systems.Count && index < compactPositions.Length; index += 1)
|
for (var index = 0; index < systems.Count && index < compactPositions.Length; index += 1)
|
||||||
@@ -165,9 +165,9 @@ public sealed partial class ScenarioLoader
|
|||||||
AsteroidField = new AsteroidFieldDefinition
|
AsteroidField = new AsteroidFieldDefinition
|
||||||
{
|
{
|
||||||
DecorationCount = template.AsteroidField.DecorationCount + ((generatedIndex % 5) * 10),
|
DecorationCount = template.AsteroidField.DecorationCount + ((generatedIndex % 5) * 10),
|
||||||
RadiusOffset = template.AsteroidField.RadiusOffset + ((generatedIndex % 4) * 18f),
|
RadiusOffset = template.AsteroidField.RadiusOffset + ((generatedIndex % 4) * 18000f),
|
||||||
RadiusVariance = template.AsteroidField.RadiusVariance + ((generatedIndex % 3) * 12f),
|
RadiusVariance = template.AsteroidField.RadiusVariance + ((generatedIndex % 3) * 12000f),
|
||||||
HeightVariance = template.AsteroidField.HeightVariance + ((generatedIndex % 4) * 4f),
|
HeightVariance = template.AsteroidField.HeightVariance + ((generatedIndex % 4) * 4000f),
|
||||||
},
|
},
|
||||||
ResourceNodes = resourceNodes,
|
ResourceNodes = resourceNodes,
|
||||||
Planets = planets,
|
Planets = planets,
|
||||||
@@ -287,14 +287,14 @@ public sealed partial class ScenarioLoader
|
|||||||
private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt)
|
private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt)
|
||||||
{
|
{
|
||||||
const int armCount = 4;
|
const int armCount = 4;
|
||||||
const float baseInnerRadius = 9000f;
|
const float baseInnerRadius = 9f;
|
||||||
const float radiusStep = 540f;
|
const float radiusStep = 0.54f;
|
||||||
const float armOffset = MathF.PI * 2f / armCount;
|
const float armOffset = MathF.PI * 2f / armCount;
|
||||||
|
|
||||||
var armIndex = (generatedIndex + attempt) % armCount;
|
var armIndex = (generatedIndex + attempt) % armCount;
|
||||||
var armDepth = generatedIndex / armCount;
|
var armDepth = generatedIndex / armCount;
|
||||||
var radius = baseInnerRadius + (armDepth * radiusStep) + Jitter(generatedIndex * 17 + attempt, 0, 900f);
|
var radius = baseInnerRadius + (armDepth * radiusStep) + Jitter(generatedIndex * 17 + attempt, 0, 0.9f);
|
||||||
var angle = (armIndex * armOffset) + (radius / 8200f) + Jitter(generatedIndex, 1 + attempt, 0.16f);
|
var angle = (armIndex * armOffset) + (radius / 8.2f) + Jitter(generatedIndex, 1 + attempt, 0.16f);
|
||||||
var x = MathF.Cos(angle) * radius;
|
var x = MathF.Cos(angle) * radius;
|
||||||
var z = MathF.Sin(angle) * radius * 0.58f;
|
var z = MathF.Sin(angle) * radius * 0.58f;
|
||||||
var y = ComputeSystemHeight(radius, generatedIndex, attempt);
|
var y = ComputeSystemHeight(radius, generatedIndex, attempt);
|
||||||
@@ -304,9 +304,9 @@ public sealed partial class ScenarioLoader
|
|||||||
private static Vector3 ComputeFallbackGeneratedSystemPosition(int generatedIndex)
|
private static Vector3 ComputeFallbackGeneratedSystemPosition(int generatedIndex)
|
||||||
{
|
{
|
||||||
const int ringCount = 5;
|
const int ringCount = 5;
|
||||||
const float fallbackRadius = 42000f;
|
const float fallbackRadius = 42f;
|
||||||
var angle = (generatedIndex % ringCount) * (MathF.PI * 2f / ringCount) + (generatedIndex / ringCount) * 0.22f;
|
var angle = (generatedIndex % ringCount) * (MathF.PI * 2f / ringCount) + (generatedIndex / ringCount) * 0.22f;
|
||||||
var radius = fallbackRadius + (generatedIndex / ringCount) * 1800f;
|
var radius = fallbackRadius + (generatedIndex / ringCount) * 1.8f;
|
||||||
return new Vector3(
|
return new Vector3(
|
||||||
MathF.Cos(angle) * radius,
|
MathF.Cos(angle) * radius,
|
||||||
ComputeSystemHeight(radius, generatedIndex, 99),
|
ComputeSystemHeight(radius, generatedIndex, 99),
|
||||||
@@ -334,7 +334,7 @@ public sealed partial class ScenarioLoader
|
|||||||
{
|
{
|
||||||
SourceKind = "asteroid-belt",
|
SourceKind = "asteroid-belt",
|
||||||
Angle = ((MathF.PI * 2f) / nodeCount) * index + Jitter(generatedIndex, 180 + index, 0.22f),
|
Angle = ((MathF.PI * 2f) / nodeCount) * index + Jitter(generatedIndex, 180 + index, 0.22f),
|
||||||
RadiusOffset = 120f + Jitter(generatedIndex, 200 + index, 36f),
|
RadiusOffset = 120000f + Jitter(generatedIndex, 200 + index, 36000f),
|
||||||
InclinationDegrees = Jitter(generatedIndex, 280 + index, 12f),
|
InclinationDegrees = Jitter(generatedIndex, 280 + index, 12f),
|
||||||
AnchorPlanetIndex = ResolveAsteroidAnchorPlanetIndex(planets),
|
AnchorPlanetIndex = ResolveAsteroidAnchorPlanetIndex(planets),
|
||||||
OreAmount = oreAmount,
|
OreAmount = oreAmount,
|
||||||
@@ -374,7 +374,7 @@ public sealed partial class ScenarioLoader
|
|||||||
{
|
{
|
||||||
SourceKind = "gas-cloud",
|
SourceKind = "gas-cloud",
|
||||||
Angle = gasAnchor.OrbitPhaseAtEpoch * (MathF.PI / 180f) + (((MathF.PI * 2f) / nodeCount) * index) + Jitter(generatedIndex, 240 + index, 0.18f),
|
Angle = gasAnchor.OrbitPhaseAtEpoch * (MathF.PI / 180f) + (((MathF.PI * 2f) / nodeCount) * index) + Jitter(generatedIndex, 240 + index, 0.18f),
|
||||||
RadiusOffset = 170f + Jitter(generatedIndex, 260 + index, 44f),
|
RadiusOffset = 170000f + Jitter(generatedIndex, 260 + index, 44000f),
|
||||||
InclinationDegrees = Jitter(generatedIndex, 320 + index, 10f),
|
InclinationDegrees = Jitter(generatedIndex, 320 + index, 10f),
|
||||||
AnchorPlanetIndex = gasAnchorIndex,
|
AnchorPlanetIndex = gasAnchorIndex,
|
||||||
OreAmount = gasAmount,
|
OreAmount = gasAmount,
|
||||||
@@ -415,7 +415,7 @@ public sealed partial class ScenarioLoader
|
|||||||
{
|
{
|
||||||
var planetCount = 2 + (int)MathF.Floor(Hash01(generatedIndex, 2) * 7f);
|
var planetCount = 2 + (int)MathF.Floor(Hash01(generatedIndex, 2) * 7f);
|
||||||
var planets = new List<PlanetDefinition>(planetCount);
|
var planets = new List<PlanetDefinition>(planetCount);
|
||||||
var orbitRadius = 140f + (Hash01(generatedIndex, 3) * 35f);
|
var orbitRadius = 0.24f + (Hash01(generatedIndex, 3) * 0.12f);
|
||||||
var sourcePlanets = template.Planets.Count > 0 ? template.Planets : null;
|
var sourcePlanets = template.Planets.Count > 0 ? template.Planets : null;
|
||||||
|
|
||||||
for (var index = 0; index < planetCount; index += 1)
|
for (var index = 0; index < planetCount; index += 1)
|
||||||
@@ -437,13 +437,13 @@ public sealed partial class ScenarioLoader
|
|||||||
Shape = profile.Shape,
|
Shape = profile.Shape,
|
||||||
MoonCount = profile.BaseMoonCount + moonVariance,
|
MoonCount = profile.BaseMoonCount + moonVariance,
|
||||||
OrbitRadius = orbitRadius,
|
OrbitRadius = orbitRadius,
|
||||||
OrbitSpeed = 0.22f / MathF.Sqrt(MathF.Max(1f, orbitRadius / 120f)),
|
OrbitSpeed = 0.11f / MathF.Sqrt(MathF.Max(orbitRadius * orbitRadius * orbitRadius, 0.02f)),
|
||||||
OrbitEccentricity = orbitEccentricity,
|
OrbitEccentricity = orbitEccentricity,
|
||||||
OrbitInclination = orbitInclination,
|
OrbitInclination = orbitInclination,
|
||||||
OrbitLongitudeOfAscendingNode = Hash01(generatedIndex, 120 + index) * 360f,
|
OrbitLongitudeOfAscendingNode = Hash01(generatedIndex, 120 + index) * 360f,
|
||||||
OrbitArgumentOfPeriapsis = Hash01(generatedIndex, 140 + index) * 360f,
|
OrbitArgumentOfPeriapsis = Hash01(generatedIndex, 140 + index) * 360f,
|
||||||
OrbitPhaseAtEpoch = Hash01(generatedIndex, 160 + index) * 360f,
|
OrbitPhaseAtEpoch = Hash01(generatedIndex, 160 + index) * 360f,
|
||||||
Size = profile.BaseSize + (Hash01(generatedIndex, 50 + index) * 10f),
|
Size = profile.BaseSize + (Hash01(generatedIndex, 50 + index) * (profile.BaseSize * 0.35f)),
|
||||||
Color = templatePlanet?.Color ?? profile.Color,
|
Color = templatePlanet?.Color ?? profile.Color,
|
||||||
Tilt = -0.45f + (Hash01(generatedIndex, 60 + index) * 0.9f),
|
Tilt = -0.45f + (Hash01(generatedIndex, 60 + index) * 0.9f),
|
||||||
HasRing = profile.CanHaveRing && Hash01(generatedIndex, 70 + index) > 0.55f,
|
HasRing = profile.CanHaveRing && Hash01(generatedIndex, 70 + index) > 0.55f,
|
||||||
@@ -493,8 +493,8 @@ public sealed partial class ScenarioLoader
|
|||||||
|
|
||||||
private static float ComputeSystemHeight(float radius, int generatedIndex, int salt)
|
private static float ComputeSystemHeight(float radius, int generatedIndex, int salt)
|
||||||
{
|
{
|
||||||
var normalized = MathF.Min(1f, MathF.Max(0f, (radius - 8000f) / 28000f));
|
var normalized = MathF.Min(1f, MathF.Max(0f, (radius - 8f) / 28f));
|
||||||
var band = 220f + (normalized * 760f);
|
var band = 0.22f + (normalized * 0.76f);
|
||||||
return (Hash01(generatedIndex, 100 + salt) * 2f - 1f) * band;
|
return (Hash01(generatedIndex, 100 + salt) * 2f - 1f) * band;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +528,7 @@ public sealed partial class ScenarioLoader
|
|||||||
int BaseMoonCount,
|
int BaseMoonCount,
|
||||||
bool CanHaveRing)
|
bool CanHaveRing)
|
||||||
{
|
{
|
||||||
public float OrbitGapMax => OrbitGapMin + 44f;
|
public float OrbitGapMax => OrbitGapMin + MathF.Max(0.12f, OrbitGapMin * 0.45f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SolarSystemDefinition CreateSolSystem()
|
private static SolarSystemDefinition CreateSolSystem()
|
||||||
@@ -546,29 +546,29 @@ public sealed partial class ScenarioLoader
|
|||||||
{
|
{
|
||||||
Id = "sol",
|
Id = "sol",
|
||||||
Label = "Sol",
|
Label = "Sol",
|
||||||
Position = [18200f, 24f, -11800f],
|
Position = [18.2f, 0.02f, -11.8f],
|
||||||
StarKind = "main-sequence",
|
StarKind = "main-sequence",
|
||||||
StarCount = 1,
|
StarCount = 1,
|
||||||
StarColor = "#fff1b8",
|
StarColor = "#fff1b8",
|
||||||
StarGlow = "#ffd35a",
|
StarGlow = "#ffd35a",
|
||||||
StarSize = 58f,
|
StarSize = 696340f,
|
||||||
GravityWellRadius = 240f,
|
GravityWellRadius = 240f,
|
||||||
AsteroidField = new AsteroidFieldDefinition
|
AsteroidField = new AsteroidFieldDefinition
|
||||||
{
|
{
|
||||||
DecorationCount = 240,
|
DecorationCount = 240,
|
||||||
RadiusOffset = ScaleSolOrbitRadiusFromAu(2.82f),
|
RadiusOffset = 422000000f,
|
||||||
RadiusVariance = 180f,
|
RadiusVariance = 180000000f,
|
||||||
HeightVariance = 22f,
|
HeightVariance = 22000000f,
|
||||||
},
|
},
|
||||||
ResourceNodes =
|
ResourceNodes =
|
||||||
[
|
[
|
||||||
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 0.2f, RadiusOffset = 126f, InclinationDegrees = 4f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
|
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 = 148f, InclinationDegrees = -6f, 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 = 138f, InclinationDegrees = 8f, AnchorPlanetIndex = 4, 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 = 164f, InclinationDegrees = -5f, 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 },
|
||||||
new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 0.9f, RadiusOffset = 210f, InclinationDegrees = 3f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "gas", ShardCount = 12 },
|
new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 0.9f, RadiusOffset = 210000f, InclinationDegrees = 3f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "gas", ShardCount = 12 },
|
||||||
new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 2.7f, RadiusOffset = 228f, InclinationDegrees = -4f, AnchorPlanetIndex = 5, OreAmount = 1000f, ItemId = "gas", ShardCount = 12 },
|
new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 2.7f, RadiusOffset = 228000f, InclinationDegrees = -4f, AnchorPlanetIndex = 5, OreAmount = 1000f, ItemId = "gas", ShardCount = 12 },
|
||||||
new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 4.8f, RadiusOffset = 186f, InclinationDegrees = 6f, AnchorPlanetIndex = 6, OreAmount = 1000f, ItemId = "gas", ShardCount = 10 },
|
new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 4.8f, RadiusOffset = 186000f, InclinationDegrees = 6f, AnchorPlanetIndex = 6, OreAmount = 1000f, ItemId = "gas", ShardCount = 10 },
|
||||||
],
|
],
|
||||||
Planets =
|
Planets =
|
||||||
[
|
[
|
||||||
@@ -605,7 +605,7 @@ public sealed partial class ScenarioLoader
|
|||||||
PlanetType = planetType,
|
PlanetType = planetType,
|
||||||
Shape = shape,
|
Shape = shape,
|
||||||
MoonCount = moonCount,
|
MoonCount = moonCount,
|
||||||
OrbitRadius = ScaleSolOrbitRadiusFromAu(orbitRadiusAu),
|
OrbitRadius = orbitRadiusAu,
|
||||||
OrbitSpeed = ComputeSolOrbitSpeed(orbitRadiusAu),
|
OrbitSpeed = ComputeSolOrbitSpeed(orbitRadiusAu),
|
||||||
OrbitEccentricity = orbitEccentricity,
|
OrbitEccentricity = orbitEccentricity,
|
||||||
OrbitInclination = orbitInclination,
|
OrbitInclination = orbitInclination,
|
||||||
@@ -614,9 +614,16 @@ public sealed partial class ScenarioLoader
|
|||||||
OrbitPhaseAtEpoch = phaseAtEpoch,
|
OrbitPhaseAtEpoch = phaseAtEpoch,
|
||||||
Size = planetType switch
|
Size = planetType switch
|
||||||
{
|
{
|
||||||
"gas-giant" => label == "Saturn" ? 66f : 72f,
|
"gas-giant" => label == "Saturn" ? 58232f : 69911f,
|
||||||
"ice-giant" => 48f,
|
"ice-giant" => label == "Uranus" ? 25362f : 24622f,
|
||||||
_ => label == "Earth" ? 28f : label == "Mars" ? 22f : label == "Venus" ? 26f : 20f,
|
_ => label switch
|
||||||
|
{
|
||||||
|
"Mercury" => 2440f,
|
||||||
|
"Venus" => 6052f,
|
||||||
|
"Earth" => 6371f,
|
||||||
|
"Mars" => 3390f,
|
||||||
|
_ => 5000f,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Color = color,
|
Color = color,
|
||||||
Tilt = tilt,
|
Tilt = tilt,
|
||||||
@@ -624,9 +631,6 @@ public sealed partial class ScenarioLoader
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float ScaleSolOrbitRadiusFromAu(float orbitRadiusAu) =>
|
|
||||||
MathF.Round(500f * MathF.Pow(orbitRadiusAu, 0.70f));
|
|
||||||
|
|
||||||
private static float ComputeSolOrbitSpeed(float orbitRadiusAu)
|
private static float ComputeSolOrbitSpeed(float orbitRadiusAu)
|
||||||
{
|
{
|
||||||
const float earthAngularSpeed = 0.11f;
|
const float earthAngularSpeed = 0.11f;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public sealed partial class ScenarioLoader
|
|||||||
parentNodeId: starNode.Id);
|
parentNodeId: starNode.Id);
|
||||||
|
|
||||||
var lagrangeNodes = new Dictionary<string, NodeRuntime>(StringComparer.Ordinal);
|
var lagrangeNodes = new Dictionary<string, NodeRuntime>(StringComparer.Ordinal);
|
||||||
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet.OrbitRadius, planet.Size, planetIndex))
|
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
|
||||||
{
|
{
|
||||||
var lagrangeNode = AddSpatialNode(
|
var lagrangeNode = AddSpatialNode(
|
||||||
nodes,
|
nodes,
|
||||||
@@ -113,39 +113,52 @@ public sealed partial class ScenarioLoader
|
|||||||
|
|
||||||
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(
|
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(
|
||||||
Vector3 planetPosition,
|
Vector3 planetPosition,
|
||||||
float orbitRadius,
|
PlanetDefinition planet)
|
||||||
float planetSize,
|
|
||||||
int planetIndex)
|
|
||||||
{
|
{
|
||||||
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
|
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
|
||||||
var tangential = new Vector3(-radial.Z, 0f, radial.X);
|
var tangential = new Vector3(-radial.Z, 0f, radial.X);
|
||||||
var offset = ComputePlanetLocalLagrangeOffset(orbitRadius, planetSize, planetIndex);
|
var orbitRadiusKm = MathF.Sqrt(planetPosition.X * planetPosition.X + planetPosition.Z * planetPosition.Z);
|
||||||
|
var offset = ComputePlanetLocalLagrangeOffset(orbitRadiusKm, planet);
|
||||||
var triangularAngle = MathF.PI / 3f;
|
var triangularAngle = MathF.PI / 3f;
|
||||||
|
|
||||||
yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
|
yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
|
||||||
yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
|
yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
|
||||||
yield return new LagrangePointPlacement("L3", Add(planetPosition, Scale(radial, -(offset * 1.2f))));
|
yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadiusKm));
|
||||||
yield return new LagrangePointPlacement(
|
yield return new LagrangePointPlacement(
|
||||||
"L4",
|
"L4",
|
||||||
Add(
|
Add(
|
||||||
planetPosition,
|
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
|
||||||
Add(
|
Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle))));
|
||||||
Scale(radial, offset * MathF.Cos(triangularAngle)),
|
|
||||||
Scale(tangential, offset * MathF.Sin(triangularAngle)))));
|
|
||||||
yield return new LagrangePointPlacement(
|
yield return new LagrangePointPlacement(
|
||||||
"L5",
|
"L5",
|
||||||
Add(
|
Add(
|
||||||
planetPosition,
|
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
|
||||||
Add(
|
Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle))));
|
||||||
Scale(radial, offset * MathF.Cos(triangularAngle)),
|
|
||||||
Scale(tangential, -offset * MathF.Sin(triangularAngle)))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float ComputePlanetLocalLagrangeOffset(float orbitRadius, float planetSize, int planetIndex)
|
private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet)
|
||||||
{
|
{
|
||||||
var orbitalScale = MathF.Min(orbitRadius * 0.016f, 96f + (planetIndex * 4f));
|
var planetMassProxy = EstimatePlanetMassRatio(planet);
|
||||||
var sizeScale = (planetSize * 1.9f) + 10f;
|
var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f));
|
||||||
return MathF.Max(22f + (planetIndex * 2f), MathF.Max(orbitalScale, sizeScale));
|
var minimumOffset = MathF.Max(planet.Size * 4f, 25000f);
|
||||||
|
return MathF.Max(minimumOffset, hillLikeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The simulation does not track physical masses yet, so use a size/density proxy.
|
||||||
|
private static float EstimatePlanetMassRatio(PlanetDefinition planet)
|
||||||
|
{
|
||||||
|
var earthRadiusRatio = MathF.Max(planet.Size / 6371f, 0.05f);
|
||||||
|
var densityFactor = planet.PlanetType switch
|
||||||
|
{
|
||||||
|
"gas-giant" => 0.24f,
|
||||||
|
"ice-giant" => 0.18f,
|
||||||
|
"oceanic" => 0.95f,
|
||||||
|
"ice" => 0.7f,
|
||||||
|
_ => 1f,
|
||||||
|
};
|
||||||
|
|
||||||
|
var earthMasses = MathF.Pow(earthRadiusRatio, 3f) * densityFactor;
|
||||||
|
return earthMasses / 332_946f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StationPlacement ResolveStationPlacement(
|
private static StationPlacement ResolveStationPlacement(
|
||||||
@@ -210,7 +223,7 @@ public sealed partial class ScenarioLoader
|
|||||||
|
|
||||||
private static Vector3 ComputeResourceNodePosition(NodeRuntime? anchorNode, ResourceNodeDefinition definition, float yPlane)
|
private static Vector3 ComputeResourceNodePosition(NodeRuntime? anchorNode, ResourceNodeDefinition definition, float yPlane)
|
||||||
{
|
{
|
||||||
var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.18f, 28f);
|
var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f);
|
||||||
var offset = new Vector3(
|
var offset = new Vector3(
|
||||||
MathF.Cos(definition.Angle) * definition.RadiusOffset,
|
MathF.Cos(definition.Angle) * definition.RadiusOffset,
|
||||||
verticalOffset,
|
verticalOffset,
|
||||||
@@ -227,8 +240,9 @@ public sealed partial class ScenarioLoader
|
|||||||
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
|
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
|
||||||
{
|
{
|
||||||
var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
|
var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
|
||||||
var x = MathF.Cos(angle) * planet.OrbitRadius;
|
var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius);
|
||||||
var z = MathF.Sin(angle) * planet.OrbitRadius;
|
var x = MathF.Cos(angle) * orbitRadiusKm;
|
||||||
|
var z = MathF.Sin(angle) * orbitRadiusKm;
|
||||||
return new Vector3(x, 0f, z);
|
return new Vector3(x, 0f, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public sealed partial class ScenarioLoader
|
|||||||
private const float MinimumRefineryOre = 0f;
|
private const float MinimumRefineryOre = 0f;
|
||||||
private const float MinimumRefineryStock = 0f;
|
private const float MinimumRefineryStock = 0f;
|
||||||
private const float MinimumShipyardStock = 0f;
|
private const float MinimumShipyardStock = 0f;
|
||||||
private const float MinimumSystemSeparation = 3200f;
|
private const float MinimumSystemSeparation = 3.2f;
|
||||||
private const float StarBubbleRadiusPadding = 40f;
|
private const float StarBubbleRadiusPadding = 40f;
|
||||||
private const float PlanetBubbleRadiusPadding = 80f;
|
private const float PlanetBubbleRadiusPadding = 80f;
|
||||||
private const float MoonBubbleRadiusPadding = 40f;
|
private const float MoonBubbleRadiusPadding = 40f;
|
||||||
@@ -55,24 +55,24 @@ public sealed partial class ScenarioLoader
|
|||||||
];
|
];
|
||||||
private static readonly StarProfile[] StarProfiles =
|
private static readonly StarProfile[] StarProfiles =
|
||||||
[
|
[
|
||||||
new("main-sequence", "#ffd27a", "#ffb14a", 54f, 1),
|
new("main-sequence", "#ffd27a", "#ffb14a", 696340f, 1),
|
||||||
new("blue-white", "#9dc6ff", "#66a0ff", 50f, 1),
|
new("blue-white", "#9dc6ff", "#66a0ff", 930000f, 1),
|
||||||
new("white-dwarf", "#f1f5ff", "#b8caff", 26f, 1),
|
new("white-dwarf", "#f1f5ff", "#b8caff", 12000f, 1),
|
||||||
new("brown-dwarf", "#b97d56", "#8a5438", 20f, 1),
|
new("brown-dwarf", "#b97d56", "#8a5438", 70000f, 1),
|
||||||
new("neutron-star", "#d9ebff", "#7ab4ff", 18f, 1),
|
new("neutron-star", "#d9ebff", "#7ab4ff", 18f, 1),
|
||||||
new("binary-main-sequence", "#ffe09f", "#ffbe6b", 64f, 2),
|
new("binary-main-sequence", "#ffe09f", "#ffbe6b", 780000f, 2),
|
||||||
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 34f, 2),
|
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f, 2),
|
||||||
];
|
];
|
||||||
private static readonly PlanetProfile[] PlanetProfiles =
|
private static readonly PlanetProfile[] PlanetProfiles =
|
||||||
[
|
[
|
||||||
new("barren", "sphere", "#bca48f", 18f, 38f, 0, false),
|
new("barren", "sphere", "#bca48f", 2800f, 0.22f, 0, false),
|
||||||
new("terrestrial", "sphere", "#58a36c", 24f, 46f, 1, false),
|
new("terrestrial", "sphere", "#58a36c", 6400f, 0.28f, 1, false),
|
||||||
new("oceanic", "sphere", "#4f84c4", 26f, 44f, 2, false),
|
new("oceanic", "sphere", "#4f84c4", 7000f, 0.30f, 2, false),
|
||||||
new("desert", "sphere", "#d4a373", 22f, 42f, 0, false),
|
new("desert", "sphere", "#d4a373", 5200f, 0.26f, 0, false),
|
||||||
new("ice", "sphere", "#c8e4ff", 24f, 40f, 1, false),
|
new("ice", "sphere", "#c8e4ff", 5800f, 0.32f, 1, false),
|
||||||
new("gas-giant", "oblate", "#d9b06f", 52f, 86f, 8, true),
|
new("gas-giant", "oblate", "#d9b06f", 45000f, 1.40f, 8, true),
|
||||||
new("ice-giant", "oblate", "#8fc0d8", 44f, 72f, 5, true),
|
new("ice-giant", "oblate", "#8fc0d8", 25000f, 1.00f, 5, true),
|
||||||
new("lava", "sphere", "#db6846", 20f, 36f, 0, false),
|
new("lava", "sphere", "#db6846", 3200f, 0.20f, 0, false),
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly string _dataRoot;
|
private readonly string _dataRoot;
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ namespace SpaceGame.Simulation.Api.Simulation;
|
|||||||
|
|
||||||
public sealed partial class SimulationEngine
|
public sealed partial class SimulationEngine
|
||||||
{
|
{
|
||||||
|
private const float WarpEngageDistanceKilometers = 250_000f;
|
||||||
|
|
||||||
|
private static float GetLocalTravelSpeed(ShipRuntime ship) =>
|
||||||
|
SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed);
|
||||||
|
|
||||||
|
private static float GetWarpTravelSpeed(ShipRuntime ship) =>
|
||||||
|
SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed);
|
||||||
|
|
||||||
|
private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) =>
|
||||||
|
world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position
|
||||||
|
?? Vector3.Zero;
|
||||||
|
|
||||||
private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||||
{
|
{
|
||||||
var task = ship.ControllerTask;
|
var task = ship.ControllerTask;
|
||||||
@@ -140,7 +152,7 @@ public sealed partial class SimulationEngine
|
|||||||
|
|
||||||
ship.ActionTimer = 0f;
|
ship.ActionTimer = 0f;
|
||||||
ship.State = ShipState.LocalFlight;
|
ship.State = ShipState.LocalFlight;
|
||||||
ship.Position = ship.Position.MoveToward(targetPosition, ship.Definition.Speed * deltaSeconds);
|
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +211,7 @@ public sealed partial class SimulationEngine
|
|||||||
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
|
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
|
||||||
? ship.Position.DistanceTo(targetPosition)
|
? ship.Position.DistanceTo(targetPosition)
|
||||||
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
|
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
|
||||||
ship.Position = ship.Position.MoveToward(targetPosition, ship.Definition.Speed * deltaSeconds);
|
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
|
||||||
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
||||||
return ship.Position.DistanceTo(targetPosition) <= 18f
|
return ship.Position.DistanceTo(targetPosition) <= 18f
|
||||||
? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode)
|
? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode)
|
||||||
@@ -258,10 +270,11 @@ public sealed partial class SimulationEngine
|
|||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalDistance = MathF.Max(0.001f, ship.Position.DistanceTo(targetPosition));
|
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
|
||||||
ship.Position = ship.Position.MoveToward(targetPosition, ship.Definition.FtlSpeed * deltaSeconds);
|
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
|
||||||
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
|
||||||
return ship.Position.DistanceTo(targetPosition) <= 24f
|
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance));
|
||||||
|
return transit.Progress >= 0.999f
|
||||||
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode)
|
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode)
|
||||||
: "none";
|
: "none";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public sealed partial class SimulationEngine
|
|||||||
var eccentricAnomaly = meanAnomaly
|
var eccentricAnomaly = meanAnomaly
|
||||||
+ (eccentricity * MathF.Sin(meanAnomaly))
|
+ (eccentricity * MathF.Sin(meanAnomaly))
|
||||||
+ (0.5f * eccentricity * eccentricity * MathF.Sin(2f * meanAnomaly));
|
+ (0.5f * eccentricity * eccentricity * MathF.Sin(2f * meanAnomaly));
|
||||||
var semiMajorAxis = planet.OrbitRadius;
|
var semiMajorAxis = SimulationUnits.AuToKilometers(planet.OrbitRadius);
|
||||||
var semiMinorAxis = semiMajorAxis * MathF.Sqrt(MathF.Max(1f - (eccentricity * eccentricity), 0.05f));
|
var semiMinorAxis = semiMajorAxis * MathF.Sqrt(MathF.Max(1f - (eccentricity * eccentricity), 0.05f));
|
||||||
var local = new Vector3(
|
var local = new Vector3(
|
||||||
semiMajorAxis * (MathF.Cos(eccentricAnomaly) - eccentricity),
|
semiMajorAxis * (MathF.Cos(eccentricAnomaly) - eccentricity),
|
||||||
@@ -58,7 +58,7 @@ public sealed partial class SimulationEngine
|
|||||||
private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node)
|
private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node)
|
||||||
{
|
{
|
||||||
var baseSpeed = node.SourceKind == "gas-cloud" ? 0.16f : 0.24f;
|
var baseSpeed = node.SourceKind == "gas-cloud" ? 0.16f : 0.24f;
|
||||||
return baseSpeed / MathF.Sqrt(MathF.Max(node.OrbitRadius / 180f, 0.45f));
|
return baseSpeed / MathF.Sqrt(MathF.Max(node.OrbitRadius / 180000f, 0.45f));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector3 ComputeResourceNodeOffset(ResourceNodeRuntime node, float timeSeconds)
|
private static Vector3 ComputeResourceNodeOffset(ResourceNodeRuntime node, float timeSeconds)
|
||||||
@@ -73,39 +73,51 @@ public sealed partial class SimulationEngine
|
|||||||
|
|
||||||
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(
|
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(
|
||||||
Vector3 planetPosition,
|
Vector3 planetPosition,
|
||||||
float orbitRadius,
|
PlanetDefinition planet)
|
||||||
float planetSize,
|
|
||||||
int planetIndex)
|
|
||||||
{
|
{
|
||||||
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
|
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
|
||||||
var tangential = new Vector3(-radial.Z, 0f, radial.X);
|
var tangential = new Vector3(-radial.Z, 0f, radial.X);
|
||||||
var offset = ComputePlanetLocalLagrangeOffset(orbitRadius, planetSize, planetIndex);
|
var orbitRadiusKm = MathF.Sqrt(planetPosition.X * planetPosition.X + planetPosition.Z * planetPosition.Z);
|
||||||
|
var offset = ComputePlanetLocalLagrangeOffset(orbitRadiusKm, planet);
|
||||||
var triangularAngle = MathF.PI / 3f;
|
var triangularAngle = MathF.PI / 3f;
|
||||||
|
|
||||||
yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
|
yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
|
||||||
yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
|
yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
|
||||||
yield return new LagrangePointPlacement("L3", Add(planetPosition, Scale(radial, -(offset * 1.2f))));
|
yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadiusKm));
|
||||||
yield return new LagrangePointPlacement(
|
yield return new LagrangePointPlacement(
|
||||||
"L4",
|
"L4",
|
||||||
Add(
|
Add(
|
||||||
planetPosition,
|
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
|
||||||
Add(
|
Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle))));
|
||||||
Scale(radial, offset * MathF.Cos(triangularAngle)),
|
|
||||||
Scale(tangential, offset * MathF.Sin(triangularAngle)))));
|
|
||||||
yield return new LagrangePointPlacement(
|
yield return new LagrangePointPlacement(
|
||||||
"L5",
|
"L5",
|
||||||
Add(
|
Add(
|
||||||
planetPosition,
|
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
|
||||||
Add(
|
Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle))));
|
||||||
Scale(radial, offset * MathF.Cos(triangularAngle)),
|
|
||||||
Scale(tangential, -offset * MathF.Sin(triangularAngle)))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float ComputePlanetLocalLagrangeOffset(float orbitRadius, float planetSize, int planetIndex)
|
private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet)
|
||||||
{
|
{
|
||||||
var orbitalScale = MathF.Min(orbitRadius * 0.016f, 96f + (planetIndex * 4f));
|
var planetMassProxy = EstimatePlanetMassRatio(planet);
|
||||||
var sizeScale = (planetSize * 1.9f) + 10f;
|
var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f));
|
||||||
return MathF.Max(22f + (planetIndex * 2f), MathF.Max(orbitalScale, sizeScale));
|
var minimumOffset = MathF.Max(planet.Size * 4f, 25000f);
|
||||||
|
return MathF.Max(minimumOffset, hillLikeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float EstimatePlanetMassRatio(PlanetDefinition planet)
|
||||||
|
{
|
||||||
|
var earthRadiusRatio = MathF.Max(planet.Size / 6371f, 0.05f);
|
||||||
|
var densityFactor = planet.PlanetType switch
|
||||||
|
{
|
||||||
|
"gas-giant" => 0.24f,
|
||||||
|
"ice-giant" => 0.18f,
|
||||||
|
"oceanic" => 0.95f,
|
||||||
|
"ice" => 0.7f,
|
||||||
|
_ => 1f,
|
||||||
|
};
|
||||||
|
|
||||||
|
var earthMasses = MathF.Pow(earthRadiusRatio, 3f) * densityFactor;
|
||||||
|
return earthMasses / 332_946f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector3 NormalizeOrFallback(Vector3 value, Vector3 fallback)
|
private static Vector3 NormalizeOrFallback(Vector3 value, Vector3 fallback)
|
||||||
@@ -185,7 +197,7 @@ public sealed partial class SimulationEngine
|
|||||||
var planetPosition = ComputePlanetPosition(planet, worldTimeSeconds);
|
var planetPosition = ComputePlanetPosition(planet, worldTimeSeconds);
|
||||||
planetNode.Position = planetPosition;
|
planetNode.Position = planetPosition;
|
||||||
|
|
||||||
foreach (var lagrange in EnumeratePlanetLagrangePoints(planetPosition, planet.OrbitRadius, planet.Size, planetIndex))
|
foreach (var lagrange in EnumeratePlanetLagrangePoints(planetPosition, planet))
|
||||||
{
|
{
|
||||||
var lagrangeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{lagrange.Designation.ToLowerInvariant()}";
|
var lagrangeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{lagrange.Designation.ToLowerInvariant()}";
|
||||||
if (spatialNodesById.TryGetValue(lagrangeId, out var lagrangeNode))
|
if (spatialNodesById.TryGetValue(lagrangeId, out var lagrangeNode))
|
||||||
|
|||||||
@@ -260,7 +260,9 @@ public sealed partial class SimulationEngine
|
|||||||
{
|
{
|
||||||
var destinationEntryNode = ResolveSystemEntryNode(world, toSystemId);
|
var destinationEntryNode = ResolveSystemEntryNode(world, toSystemId);
|
||||||
var destinationEntryPosition = destinationEntryNode?.Position ?? toPosition;
|
var destinationEntryPosition = destinationEntryNode?.Position ?? toPosition;
|
||||||
var ftlDistance = fromPosition.DistanceTo(destinationEntryPosition);
|
var originSystemPosition = ResolveSystemGalaxyPosition(world, fromSystemId);
|
||||||
|
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, toSystemId);
|
||||||
|
var ftlDistance = originSystemPosition.DistanceTo(destinationSystemPosition);
|
||||||
var ftlDuration = ftlDistance / MathF.Max(ship.Definition.FtlSpeed, 0.01f);
|
var ftlDuration = ftlDistance / MathF.Max(ship.Definition.FtlSpeed, 0.01f);
|
||||||
return EstimateTimedEnergyUse(world, ship.Definition.SpoolTime, world.Balance.Energy.IdleDrain)
|
return EstimateTimedEnergyUse(world, ship.Definition.SpoolTime, world.Balance.Energy.IdleDrain)
|
||||||
+ EstimateTimedEnergyUse(world, ftlDuration, world.Balance.Energy.WarpDrain)
|
+ EstimateTimedEnergyUse(world, ftlDuration, world.Balance.Energy.WarpDrain)
|
||||||
@@ -278,14 +280,14 @@ public sealed partial class SimulationEngine
|
|||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distance <= 120f)
|
if (distance <= WarpEngageDistanceKilometers)
|
||||||
{
|
{
|
||||||
var localDuration = distance / MathF.Max(ship.Definition.Speed, 0.01f);
|
var localDuration = distance / MathF.Max(GetLocalTravelSpeed(ship), 0.01f);
|
||||||
return EstimateTimedEnergyUse(world, localDuration, world.Balance.Energy.MoveDrain);
|
return EstimateTimedEnergyUse(world, localDuration, world.Balance.Energy.MoveDrain);
|
||||||
}
|
}
|
||||||
|
|
||||||
var warpSpoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
var warpSpoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
||||||
var warpDuration = distance / MathF.Max(ship.Definition.Speed, 0.01f);
|
var warpDuration = distance / MathF.Max(GetWarpTravelSpeed(ship), 0.01f);
|
||||||
return EstimateTimedEnergyUse(world, warpSpoolDuration, world.Balance.Energy.IdleDrain)
|
return EstimateTimedEnergyUse(world, warpSpoolDuration, world.Balance.Energy.IdleDrain)
|
||||||
+ EstimateTimedEnergyUse(world, warpDuration, world.Balance.Energy.WarpDrain);
|
+ EstimateTimedEnergyUse(world, warpDuration, world.Balance.Energy.WarpDrain);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ public sealed partial class SimulationEngine
|
|||||||
ship.CargoItemId,
|
ship.CargoItemId,
|
||||||
ship.WorkerPopulation,
|
ship.WorkerPopulation,
|
||||||
ship.EnergyStored,
|
ship.EnergyStored,
|
||||||
|
ship.TravelSpeed,
|
||||||
|
ship.TravelSpeedUnit,
|
||||||
ship.Inventory,
|
ship.Inventory,
|
||||||
ship.FactionId,
|
ship.FactionId,
|
||||||
ship.Health,
|
ship.Health,
|
||||||
@@ -670,6 +672,8 @@ public sealed partial class SimulationEngine
|
|||||||
ship.Definition.CargoItemId,
|
ship.Definition.CargoItemId,
|
||||||
ship.WorkerPopulation,
|
ship.WorkerPopulation,
|
||||||
ship.EnergyStored,
|
ship.EnergyStored,
|
||||||
|
ToShipTravelSpeed(ship).Speed,
|
||||||
|
ToShipTravelSpeed(ship).Unit,
|
||||||
ToInventoryEntries(ship.Inventory),
|
ToInventoryEntries(ship.Inventory),
|
||||||
ship.FactionId,
|
ship.FactionId,
|
||||||
ship.Health,
|
ship.Health,
|
||||||
@@ -712,6 +716,16 @@ public sealed partial class SimulationEngine
|
|||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (float Speed, string Unit) ToShipTravelSpeed(ShipRuntime ship)
|
||||||
|
{
|
||||||
|
return ship.SpatialState.MovementRegime switch
|
||||||
|
{
|
||||||
|
MovementRegimeKinds.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"),
|
||||||
|
MovementRegimeKinds.Warp => (ship.State == ShipState.Warping ? ship.Definition.WarpSpeed : 0f, "AU/s"),
|
||||||
|
_ => (MathF.Sqrt(MathF.Max(0f, ship.Velocity.LengthSquared())) * SimulationUnits.MetersPerKilometer, "m/s"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static ShipActionProgressSnapshot CreateShipActionProgress(string label, float elapsedSeconds, float requiredSeconds) =>
|
private static ShipActionProgressSnapshot CreateShipActionProgress(string label, float elapsedSeconds, float requiredSeconds) =>
|
||||||
new(label, Math.Clamp(elapsedSeconds / requiredSeconds, 0f, 1f));
|
new(label, Math.Clamp(elapsedSeconds / requiredSeconds, 0f, 1f));
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public sealed partial class SimulationEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
ship.State = ShipState.MiningApproach;
|
ship.State = ShipState.MiningApproach;
|
||||||
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, ship.Definition.Speed * deltaSeconds);
|
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ public sealed partial class SimulationEngine
|
|||||||
var waitDistance = ship.Position.DistanceTo(ship.TargetPosition);
|
var waitDistance = ship.Position.DistanceTo(ship.TargetPosition);
|
||||||
if (waitDistance > 4f && TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
|
if (waitDistance > 4f && TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
|
||||||
{
|
{
|
||||||
ship.Position = ship.Position.MoveToward(ship.TargetPosition, ship.Definition.Speed * deltaSeconds);
|
ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return "none";
|
return "none";
|
||||||
@@ -144,7 +144,7 @@ public sealed partial class SimulationEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
ship.State = ShipState.DockingApproach;
|
ship.State = ShipState.DockingApproach;
|
||||||
ship.Position = ship.Position.MoveToward(padPosition, ship.Definition.Speed * deltaSeconds);
|
ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ public sealed partial class SimulationEngine
|
|||||||
{
|
{
|
||||||
ship.State = ShipState.LocalFlight;
|
ship.State = ShipState.LocalFlight;
|
||||||
ship.TargetPosition = supportPosition;
|
ship.TargetPosition = supportPosition;
|
||||||
ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds);
|
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +340,7 @@ public sealed partial class SimulationEngine
|
|||||||
{
|
{
|
||||||
ship.State = ShipState.LocalFlight;
|
ship.State = ShipState.LocalFlight;
|
||||||
ship.TargetPosition = supportPosition;
|
ship.TargetPosition = supportPosition;
|
||||||
ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds);
|
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ public sealed partial class SimulationEngine
|
|||||||
{
|
{
|
||||||
ship.State = ShipState.LocalFlight;
|
ship.State = ShipState.LocalFlight;
|
||||||
ship.TargetPosition = supportPosition;
|
ship.TargetPosition = supportPosition;
|
||||||
ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds);
|
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +476,7 @@ public sealed partial class SimulationEngine
|
|||||||
{
|
{
|
||||||
ship.State = ShipState.LocalFlight;
|
ship.State = ShipState.LocalFlight;
|
||||||
ship.TargetPosition = supportPosition;
|
ship.TargetPosition = supportPosition;
|
||||||
ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds);
|
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ export class ViewerAppController {
|
|||||||
return this.sceneDataController.createWorldPresentationContext({
|
return this.sceneDataController.createWorldPresentationContext({
|
||||||
world: this.world,
|
world: this.world,
|
||||||
activeSystemId: this.activeSystemId,
|
activeSystemId: this.activeSystemId,
|
||||||
|
zoomLevel: this.zoomLevel,
|
||||||
orbitYaw: this.orbitYaw,
|
orbitYaw: this.orbitYaw,
|
||||||
camera: this.camera,
|
camera: this.camera,
|
||||||
systemFocusLocal: this.systemFocusLocal,
|
systemFocusLocal: this.systemFocusLocal,
|
||||||
@@ -337,6 +338,7 @@ export class ViewerAppController {
|
|||||||
this.keyState,
|
this.keyState,
|
||||||
this.orbitYaw,
|
this.orbitYaw,
|
||||||
this.currentDistance,
|
this.currentDistance,
|
||||||
|
this.zoomLevel,
|
||||||
this.activeSystemId,
|
this.activeSystemId,
|
||||||
this.systemFocusLocal,
|
this.systemFocusLocal,
|
||||||
this.galaxyFocus,
|
this.galaxyFocus,
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export interface ShipSnapshot {
|
|||||||
cargoItemId?: string | null;
|
cargoItemId?: string | null;
|
||||||
workerPopulation: number;
|
workerPopulation: number;
|
||||||
energyStored: number;
|
energyStored: number;
|
||||||
|
travelSpeed: number;
|
||||||
|
travelSpeedUnit: string;
|
||||||
inventory: InventoryEntry[];
|
inventory: InventoryEntry[];
|
||||||
factionId: string;
|
factionId: string;
|
||||||
health: number;
|
health: number;
|
||||||
|
|||||||
@@ -68,14 +68,14 @@ canvas {
|
|||||||
.hover-label {
|
.hover-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border-radius: 999px;
|
border-radius: 14px;
|
||||||
background: rgba(7, 15, 28, 0.88);
|
background: rgba(7, 15, 28, 0.88);
|
||||||
border: 1px solid rgba(255, 88, 72, 0.5);
|
border: 1px solid rgba(255, 88, 72, 0.5);
|
||||||
color: #fff2ef;
|
color: #fff2ef;
|
||||||
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1;
|
line-height: 1.35;
|
||||||
white-space: nowrap;
|
white-space: pre-line;
|
||||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { ACTIVE_SYSTEM_CAPTURE_RADIUS, ACTIVE_SYSTEM_DETAIL_SCALE, GALAXY_PARALLAX_FACTOR } from "./viewerConstants";
|
import { ACTIVE_SYSTEM_CAPTURE_RADIUS, ACTIVE_SYSTEM_DETAIL_SCALE, GALAXY_PARALLAX_FACTOR } from "./viewerConstants";
|
||||||
import { computePlanetLocalPosition, currentWorldTimeSeconds, toThreeVector } from "./viewerMath";
|
import { KILOMETERS_PER_AU, computePlanetLocalPosition, currentWorldTimeSeconds, scaleGalaxyVector, scaleLocalVector, toThreeVector } from "./viewerMath";
|
||||||
import { resolveSelectableSystemId } from "./viewerSelection";
|
import { resolveSelectableSystemId } from "./viewerSelection";
|
||||||
import type {
|
import type {
|
||||||
BubbleVisual,
|
BubbleVisual,
|
||||||
@@ -13,6 +13,7 @@ import type {
|
|||||||
SpatialNodeVisual,
|
SpatialNodeVisual,
|
||||||
StructureVisual,
|
StructureVisual,
|
||||||
WorldState,
|
WorldState,
|
||||||
|
ZoomLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
interface ResolveSelectionPositionParams {
|
interface ResolveSelectionPositionParams {
|
||||||
@@ -75,6 +76,7 @@ export function updatePanFromKeyboard(
|
|||||||
keyState: Set<string>,
|
keyState: Set<string>,
|
||||||
orbitYaw: number,
|
orbitYaw: number,
|
||||||
currentDistance: number,
|
currentDistance: number,
|
||||||
|
zoomLevel: ZoomLevel,
|
||||||
activeSystemId: string | undefined,
|
activeSystemId: string | undefined,
|
||||||
systemFocusLocal: THREE.Vector3,
|
systemFocusLocal: THREE.Vector3,
|
||||||
galaxyFocus: THREE.Vector3,
|
galaxyFocus: THREE.Vector3,
|
||||||
@@ -103,12 +105,15 @@ export function updatePanFromKeyboard(
|
|||||||
const forward = new THREE.Vector3(Math.cos(orbitYaw), 0, Math.sin(orbitYaw));
|
const forward = new THREE.Vector3(Math.cos(orbitYaw), 0, Math.sin(orbitYaw));
|
||||||
const right = new THREE.Vector3(-forward.z, 0, forward.x);
|
const right = new THREE.Vector3(-forward.z, 0, forward.x);
|
||||||
const pan = right.multiplyScalar(move.x).add(forward.multiplyScalar(move.z));
|
const pan = right.multiplyScalar(move.x).add(forward.multiplyScalar(move.z));
|
||||||
const speed = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 320, 6800);
|
|
||||||
if (activeSystemId) {
|
if (activeSystemId) {
|
||||||
systemFocusLocal.addScaledVector(pan, speed * delta);
|
const speedKilometers = zoomLevel === "system"
|
||||||
|
? THREE.MathUtils.mapLinear(currentDistance, 80, 4000, KILOMETERS_PER_AU * 0.002, KILOMETERS_PER_AU * 0.35)
|
||||||
|
: THREE.MathUtils.mapLinear(currentDistance, minimumDistance, 120, 40, 180000);
|
||||||
|
systemFocusLocal.addScaledVector(pan, speedKilometers * delta);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const speed = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 320, 6800);
|
||||||
galaxyFocus.addScaledVector(pan, speed * delta);
|
galaxyFocus.addScaledVector(pan, speed * delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +132,14 @@ export function determineActiveSystemId(params: DetermineActiveSystemParams): st
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cameraMode === "follow" && cameraTargetShipId) {
|
if (cameraMode === "follow" && cameraTargetShipId) {
|
||||||
return world.ships.get(cameraTargetShipId)?.systemId;
|
const followedShip = world.ships.get(cameraTargetShipId);
|
||||||
|
if (!followedShip) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return followedShip.spatialState.movementRegime === "ftl-transit"
|
||||||
|
? undefined
|
||||||
|
: followedShip.systemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentDistance >= 12000) {
|
if (currentDistance >= 12000) {
|
||||||
@@ -152,7 +164,7 @@ export function determineActiveSystemId(params: DetermineActiveSystemParams): st
|
|||||||
let nearestSystemId: string | undefined;
|
let nearestSystemId: string | undefined;
|
||||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||||
for (const system of world.systems.values()) {
|
for (const system of world.systems.values()) {
|
||||||
const center = toThreeVector(system.galaxyPosition);
|
const center = scaleGalaxyVector(toThreeVector(system.galaxyPosition));
|
||||||
const distance = center.distanceTo(galaxyFocus);
|
const distance = center.distanceTo(galaxyFocus);
|
||||||
if (distance < nearestDistance) {
|
if (distance < nearestDistance) {
|
||||||
nearestDistance = distance;
|
nearestDistance = distance;
|
||||||
@@ -222,7 +234,7 @@ export function resolveSelectionPosition(params: ResolveSelectionPositionParams)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const system = world.systems.get(selection.id);
|
const system = world.systems.get(selection.id);
|
||||||
return system ? toThreeVector(system.galaxyPosition) : undefined;
|
return system ? scaleGalaxyVector(toThreeVector(system.galaxyPosition)) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusOnSelection(params: FocusOnSelectionParams) {
|
export function focusOnSelection(params: FocusOnSelectionParams) {
|
||||||
@@ -249,7 +261,7 @@ export function focusOnSelection(params: FocusOnSelectionParams) {
|
|||||||
if (selectionSystemId && world) {
|
if (selectionSystemId && world) {
|
||||||
const system = world.systems.get(selectionSystemId);
|
const system = world.systems.get(selectionSystemId);
|
||||||
if (system) {
|
if (system) {
|
||||||
galaxyFocus.copy(toThreeVector(system.galaxyPosition));
|
galaxyFocus.copy(scaleGalaxyVector(toThreeVector(system.galaxyPosition)));
|
||||||
systemFocusLocal.copy(nextFocus);
|
systemFocusLocal.copy(nextFocus);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -325,8 +337,8 @@ export function getCameraFocusWorldPosition(params: CameraFocusParams): THREE.Ve
|
|||||||
|
|
||||||
const system = world.systems.get(activeSystemId);
|
const system = world.systems.get(activeSystemId);
|
||||||
return system
|
return system
|
||||||
? toThreeVector(system.galaxyPosition).add(
|
? scaleGalaxyVector(toThreeVector(system.galaxyPosition)).add(
|
||||||
systemFocusLocal.clone().multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE * GALAXY_PARALLAX_FACTOR),
|
scaleLocalVector(systemFocusLocal).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE * GALAXY_PARALLAX_FACTOR),
|
||||||
)
|
)
|
||||||
: galaxyFocus;
|
: galaxyFocus;
|
||||||
}
|
}
|
||||||
@@ -341,18 +353,20 @@ export function toDisplayLocalPosition(params: DisplayLocalPositionParams): THRE
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (!world || !systemId) {
|
if (!world || !systemId) {
|
||||||
return localPosition.clone();
|
return scaleLocalVector(localPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
const system = world.systems.get(systemId);
|
const system = world.systems.get(systemId);
|
||||||
if (!system) {
|
if (!system) {
|
||||||
return localPosition.clone();
|
return scaleLocalVector(localPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = toThreeVector(system.galaxyPosition);
|
const center = scaleGalaxyVector(toThreeVector(system.galaxyPosition));
|
||||||
|
const scaledLocalPosition = scaleLocalVector(localPosition);
|
||||||
|
const scaledSystemFocus = scaleLocalVector(systemFocusLocal);
|
||||||
if (systemId !== activeSystemId) {
|
if (systemId !== activeSystemId) {
|
||||||
return center.clone().add(localPosition);
|
return center.clone().add(scaledLocalPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
return center.clone().add(localPosition.clone().sub(systemFocusLocal).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE));
|
return center.clone().add(scaledLocalPosition.sub(scaledSystemFocus).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { ZoomLevel } from "./viewerTypes";
|
import type { ZoomLevel } from "./viewerTypes";
|
||||||
|
|
||||||
export const ZOOM_DISTANCE: Record<ZoomLevel, number> = {
|
export const ZOOM_DISTANCE: Record<ZoomLevel, number> = {
|
||||||
local: 900,
|
local: 18,
|
||||||
system: 3200,
|
system: 3200,
|
||||||
universe: 26000,
|
universe: 32000,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ACTIVE_SYSTEM_DETAIL_SCALE = 10;
|
export const ACTIVE_SYSTEM_DETAIL_SCALE = 10;
|
||||||
@@ -13,8 +13,8 @@ export const PROJECTED_GALAXY_RADIUS = 65000;
|
|||||||
export const STAR_RENDER_SCALE = 0.18;
|
export const STAR_RENDER_SCALE = 0.18;
|
||||||
export const PLANET_RENDER_SCALE = 0.95;
|
export const PLANET_RENDER_SCALE = 0.95;
|
||||||
export const MOON_RENDER_SCALE = 1.1;
|
export const MOON_RENDER_SCALE = 1.1;
|
||||||
export const MIN_CAMERA_DISTANCE = 450;
|
export const MIN_CAMERA_DISTANCE = 2;
|
||||||
export const MAX_CAMERA_DISTANCE = 42000;
|
export const MAX_CAMERA_DISTANCE = 52000;
|
||||||
|
|
||||||
export interface ZoomBlend {
|
export interface ZoomBlend {
|
||||||
localWeight: number;
|
localWeight: number;
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export function createViewerControllers(host: any) {
|
|||||||
host.cameraTargetShipId = value;
|
host.cameraTargetShipId = value;
|
||||||
},
|
},
|
||||||
getCurrentDistance: () => host.currentDistance,
|
getCurrentDistance: () => host.currentDistance,
|
||||||
|
getZoomLevel: () => host.zoomLevel,
|
||||||
getSelectedItems: () => host.selectedItems,
|
getSelectedItems: () => host.selectedItems,
|
||||||
getOrbitYaw: () => host.orbitYaw,
|
getOrbitYaw: () => host.orbitYaw,
|
||||||
galaxyFocus: host.galaxyFocus,
|
galaxyFocus: host.galaxyFocus,
|
||||||
@@ -199,6 +200,7 @@ export function createViewerControllers(host: any) {
|
|||||||
keyState: host.keyState,
|
keyState: host.keyState,
|
||||||
getWorld: () => host.world,
|
getWorld: () => host.world,
|
||||||
getActiveSystemId: () => host.activeSystemId,
|
getActiveSystemId: () => host.activeSystemId,
|
||||||
|
getZoomLevel: () => host.zoomLevel,
|
||||||
getSelectedItems: () => host.selectedItems,
|
getSelectedItems: () => host.selectedItems,
|
||||||
setSelectedItems: (items) => {
|
setSelectedItems: (items) => {
|
||||||
host.selectedItems = items;
|
host.selectedItems = items;
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { MAX_CAMERA_DISTANCE, MIN_CAMERA_DISTANCE, ZOOM_DISTANCE } from "./viewerConstants";
|
import { MAX_CAMERA_DISTANCE, MIN_CAMERA_DISTANCE, ZOOM_DISTANCE } from "./viewerConstants";
|
||||||
|
import { scaleGalaxyVector, toDisplayGalaxyVector, toThreeVector } from "./viewerMath";
|
||||||
import { rawObject } from "./viewerScenePrimitives";
|
import { rawObject } from "./viewerScenePrimitives";
|
||||||
|
import { resolveShipWorldPosition } from "./viewerWorldPresentation";
|
||||||
import type {
|
import type {
|
||||||
CameraMode,
|
CameraMode,
|
||||||
Selectable,
|
Selectable,
|
||||||
ShipVisual,
|
ShipVisual,
|
||||||
SystemVisual,
|
SystemVisual,
|
||||||
WorldState,
|
WorldState,
|
||||||
|
ZoomLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
export function syncFollowStateFromSelection(
|
export function syncFollowStateFromSelection(
|
||||||
@@ -129,10 +132,32 @@ export function updateFollowCamera(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shipLocalPosition = getAnimatedShipLocalPosition(visual);
|
const shipLocalPosition = getAnimatedShipLocalPosition(visual);
|
||||||
const shipWorldPosition = toDisplayLocalPosition(shipLocalPosition, ship.systemId);
|
const shipWorldPosition = resolveShipWorldPosition(
|
||||||
systemFocusLocal.lerp(shipLocalPosition, 1 - Math.exp(-delta * 8));
|
{
|
||||||
|
world,
|
||||||
|
toDisplayLocalPosition,
|
||||||
|
},
|
||||||
|
ship,
|
||||||
|
visual,
|
||||||
|
shipLocalPosition,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ship.spatialState.movementRegime === "ftl-transit") {
|
||||||
|
systemFocusLocal.set(0, 0, 0);
|
||||||
|
const destinationNodeId = ship.spatialState.transit?.destinationNodeId;
|
||||||
|
const destinationNode = destinationNodeId ? world.spatialNodes.get(destinationNodeId) : undefined;
|
||||||
|
const destinationSystem = destinationNode ? world.systems.get(destinationNode.systemId) : undefined;
|
||||||
|
const originSystem = world.systems.get(ship.systemId);
|
||||||
|
if (originSystem && destinationSystem) {
|
||||||
|
followCameraDesiredDirection
|
||||||
|
.copy(scaleGalaxyVector(toThreeVector(destinationSystem.galaxyPosition)).sub(scaleGalaxyVector(toThreeVector(originSystem.galaxyPosition))))
|
||||||
|
.normalize();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
systemFocusLocal.lerp(shipLocalPosition, 1 - Math.exp(-delta * 8));
|
||||||
followCameraDesiredDirection.copy(resolveShipHeading(visual, shipLocalPosition)).normalize();
|
followCameraDesiredDirection.copy(resolveShipHeading(visual, shipLocalPosition)).normalize();
|
||||||
|
}
|
||||||
|
|
||||||
followCameraDirection.lerp(followCameraDesiredDirection, 1 - Math.exp(-delta * 5));
|
followCameraDirection.lerp(followCameraDesiredDirection, 1 - Math.exp(-delta * 5));
|
||||||
followCameraDirection.normalize();
|
followCameraDirection.normalize();
|
||||||
|
|
||||||
@@ -165,9 +190,10 @@ export function updateFollowCamera(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSystemDetailVisibility(systemVisuals: Map<string, SystemVisual>, activeSystemId?: string) {
|
export function updateSystemDetailVisibility(systemVisuals: Map<string, SystemVisual>, activeSystemId?: string, zoomLevel?: ZoomLevel) {
|
||||||
|
const detailVisible = !!activeSystemId && zoomLevel !== "universe";
|
||||||
for (const [systemId, visual] of systemVisuals.entries()) {
|
for (const [systemId, visual] of systemVisuals.entries()) {
|
||||||
visual.detailGroup.setVisible(systemId === activeSystemId);
|
visual.detailGroup.setVisible(detailVisible && systemId === activeSystemId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { getSelectionGroup } from "./viewerSelection";
|
import { describeHoverLabel, getSelectionGroup } from "./viewerSelection";
|
||||||
import type { Selectable, SelectionGroup, WorldState } from "./viewerTypes";
|
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
|
||||||
|
import { DISPLAY_UNITS_PER_KILOMETER, DISPLAY_UNITS_PER_LIGHT_YEAR, KILOMETERS_PER_AU, formatAdaptiveDistanceFromKilometers, formatSystemDistance } from "./viewerMath";
|
||||||
|
import type { Selectable, SelectionGroup, WorldState, ZoomLevel } from "./viewerTypes";
|
||||||
|
|
||||||
|
export interface HoverPickResult {
|
||||||
|
selection: Selectable;
|
||||||
|
object: THREE.Object3D;
|
||||||
|
}
|
||||||
|
|
||||||
export function pickSelectableAtClientPosition(
|
export function pickSelectableAtClientPosition(
|
||||||
renderer: THREE.WebGLRenderer,
|
renderer: THREE.WebGLRenderer,
|
||||||
@@ -11,29 +18,49 @@ export function pickSelectableAtClientPosition(
|
|||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
) {
|
) {
|
||||||
|
const hit = pickSelectableHitAtClientPosition(renderer, raycaster, mouse, camera, selectableTargets, clientX, clientY);
|
||||||
|
return hit?.selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickSelectableHitAtClientPosition(
|
||||||
|
renderer: THREE.WebGLRenderer,
|
||||||
|
raycaster: THREE.Raycaster,
|
||||||
|
mouse: THREE.Vector2,
|
||||||
|
camera: THREE.Camera,
|
||||||
|
selectableTargets: Map<THREE.Object3D, Selectable>,
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
): HoverPickResult | undefined {
|
||||||
const bounds = renderer.domElement.getBoundingClientRect();
|
const bounds = renderer.domElement.getBoundingClientRect();
|
||||||
mouse.x = ((clientX - bounds.left) / bounds.width) * 2 - 1;
|
mouse.x = ((clientX - bounds.left) / bounds.width) * 2 - 1;
|
||||||
mouse.y = -(((clientY - bounds.top) / bounds.height) * 2 - 1);
|
mouse.y = -(((clientY - bounds.top) / bounds.height) * 2 - 1);
|
||||||
raycaster.setFromCamera(mouse, camera);
|
raycaster.setFromCamera(mouse, camera);
|
||||||
const hit = raycaster.intersectObjects([...selectableTargets.keys()], false)[0];
|
const hit = raycaster.intersectObjects([...selectableTargets.keys()], false)[0];
|
||||||
return hit ? selectableTargets.get(hit.object) : undefined;
|
const selection = hit ? selectableTargets.get(hit.object) : undefined;
|
||||||
|
return hit && selection
|
||||||
|
? { selection, object: hit.object }
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateHoverLabel(params: {
|
export function updateHoverLabel(params: {
|
||||||
dragMode?: string;
|
dragMode?: string;
|
||||||
hoverLabelEl: HTMLDivElement;
|
hoverLabelEl: HTMLDivElement;
|
||||||
selection: Selectable | undefined;
|
hoverPick: HoverPickResult | undefined;
|
||||||
activeSystemId?: string;
|
activeSystemId?: string;
|
||||||
|
zoomLevel: ZoomLevel;
|
||||||
world?: WorldState;
|
world?: WorldState;
|
||||||
point: THREE.Vector2;
|
point: THREE.Vector2;
|
||||||
|
camera: THREE.Camera;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
dragMode,
|
dragMode,
|
||||||
hoverLabelEl,
|
hoverLabelEl,
|
||||||
selection,
|
hoverPick,
|
||||||
activeSystemId,
|
activeSystemId,
|
||||||
|
zoomLevel,
|
||||||
world,
|
world,
|
||||||
point,
|
point,
|
||||||
|
camera,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (dragMode) {
|
if (dragMode) {
|
||||||
@@ -41,23 +68,60 @@ export function updateHoverLabel(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selection || selection.kind !== "system" || selection.id === activeSystemId) {
|
if (!hoverPick) {
|
||||||
hoverLabelEl.hidden = true;
|
hoverLabelEl.hidden = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const system = world?.systems.get(selection.id);
|
const { selection, object } = hoverPick;
|
||||||
if (!system) {
|
const label = describeHoverLabel(world, selection);
|
||||||
|
if (!label) {
|
||||||
hoverLabelEl.hidden = true;
|
hoverLabelEl.hidden = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const distance = formatHoverDistance(camera, object, selection, zoomLevel, activeSystemId);
|
||||||
|
|
||||||
hoverLabelEl.hidden = false;
|
hoverLabelEl.hidden = false;
|
||||||
hoverLabelEl.textContent = system.label;
|
hoverLabelEl.textContent = `${label}\n${distance}`;
|
||||||
hoverLabelEl.style.left = `${point.x + 14}px`;
|
hoverLabelEl.style.left = `${point.x + 14}px`;
|
||||||
hoverLabelEl.style.top = `${point.y + 14}px`;
|
hoverLabelEl.style.top = `${point.y + 14}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatHoverDistance(
|
||||||
|
camera: THREE.Camera,
|
||||||
|
object: THREE.Object3D,
|
||||||
|
selection: Selectable,
|
||||||
|
zoomLevel: ZoomLevel,
|
||||||
|
activeSystemId?: string,
|
||||||
|
) {
|
||||||
|
const worldPosition = object.getWorldPosition(new THREE.Vector3());
|
||||||
|
const displayDistance = camera.position.distanceTo(worldPosition);
|
||||||
|
|
||||||
|
if (selection.kind === "system") {
|
||||||
|
return formatAdaptiveDistanceFromKilometers((displayDistance / DISPLAY_UNITS_PER_LIGHT_YEAR) * 9.4607e12);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inActiveSystem = selection.kind === "planet"
|
||||||
|
? selection.systemId === activeSystemId
|
||||||
|
: selection.kind === "ship"
|
||||||
|
|| selection.kind === "station"
|
||||||
|
|| selection.kind === "node"
|
||||||
|
|| selection.kind === "spatial-node"
|
||||||
|
|| selection.kind === "bubble"
|
||||||
|
|| selection.kind === "claim"
|
||||||
|
|| selection.kind === "construction-site";
|
||||||
|
|
||||||
|
if (inActiveSystem && activeSystemId) {
|
||||||
|
const kilometers = displayDistance / (DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||||
|
return zoomLevel === "system"
|
||||||
|
? formatSystemDistance(kilometers / KILOMETERS_PER_AU)
|
||||||
|
: formatAdaptiveDistanceFromKilometers(kilometers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatAdaptiveDistanceFromKilometers(displayDistance / DISPLAY_UNITS_PER_KILOMETER);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateMarqueeBox(
|
export function updateMarqueeBox(
|
||||||
marqueeEl: HTMLDivElement,
|
marqueeEl: HTMLDivElement,
|
||||||
dragStart: THREE.Vector2,
|
dragStart: THREE.Vector2,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as THREE from "three";
|
|||||||
import {
|
import {
|
||||||
completeMarqueeSelection,
|
completeMarqueeSelection,
|
||||||
hideMarqueeBox,
|
hideMarqueeBox,
|
||||||
|
pickSelectableHitAtClientPosition,
|
||||||
pickSelectableAtClientPosition,
|
pickSelectableAtClientPosition,
|
||||||
updateHoverLabel,
|
updateHoverLabel,
|
||||||
updateMarqueeBox,
|
updateMarqueeBox,
|
||||||
@@ -17,6 +18,7 @@ import type {
|
|||||||
DragMode,
|
DragMode,
|
||||||
Selectable,
|
Selectable,
|
||||||
WorldState,
|
WorldState,
|
||||||
|
ZoomLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
export interface ViewerInteractionContext {
|
export interface ViewerInteractionContext {
|
||||||
@@ -30,6 +32,7 @@ export interface ViewerInteractionContext {
|
|||||||
keyState: Set<string>;
|
keyState: Set<string>;
|
||||||
getWorld: () => WorldState | undefined;
|
getWorld: () => WorldState | undefined;
|
||||||
getActiveSystemId: () => string | undefined;
|
getActiveSystemId: () => string | undefined;
|
||||||
|
getZoomLevel: () => ZoomLevel;
|
||||||
getSelectedItems: () => Selectable[];
|
getSelectedItems: () => Selectable[];
|
||||||
setSelectedItems: (items: Selectable[]) => void;
|
setSelectedItems: (items: Selectable[]) => void;
|
||||||
getDragMode: () => DragMode | undefined;
|
getDragMode: () => DragMode | undefined;
|
||||||
@@ -247,10 +250,12 @@ export class ViewerInteractionController {
|
|||||||
updateHoverLabel({
|
updateHoverLabel({
|
||||||
dragMode: this.context.getDragMode(),
|
dragMode: this.context.getDragMode(),
|
||||||
hoverLabelEl: this.context.hoverLabelEl,
|
hoverLabelEl: this.context.hoverLabelEl,
|
||||||
selection: this.pickSelectableAtClientPosition(event.clientX, event.clientY),
|
hoverPick: this.pickSelectableHitAtClientPosition(event.clientX, event.clientY),
|
||||||
activeSystemId: this.context.getActiveSystemId(),
|
activeSystemId: this.context.getActiveSystemId(),
|
||||||
|
zoomLevel: this.context.getZoomLevel(),
|
||||||
world: this.context.getWorld(),
|
world: this.context.getWorld(),
|
||||||
point: this.context.screenPointFromClient(event.clientX, event.clientY),
|
point: this.context.screenPointFromClient(event.clientX, event.clientY),
|
||||||
|
camera: this.context.camera,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +290,18 @@ export class ViewerInteractionController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private pickSelectableHitAtClientPosition(clientX: number, clientY: number) {
|
||||||
|
return pickSelectableHitAtClientPosition(
|
||||||
|
this.context.renderer,
|
||||||
|
this.context.raycaster,
|
||||||
|
this.context.mouse,
|
||||||
|
this.context.camera,
|
||||||
|
this.context.selectableTargets,
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private completeMarqueeSelection() {
|
private completeMarqueeSelection() {
|
||||||
const selection = completeMarqueeSelection({
|
const selection = completeMarqueeSelection({
|
||||||
renderer: this.context.renderer,
|
renderer: this.context.renderer,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { MOON_RENDER_SCALE } from "./viewerConstants";
|
import { MOON_RENDER_SCALE } from "./viewerConstants";
|
||||||
import type {
|
import type {
|
||||||
|
ShipSnapshot,
|
||||||
PlanetSnapshot,
|
PlanetSnapshot,
|
||||||
Vector3Dto,
|
Vector3Dto,
|
||||||
WorldSnapshot,
|
WorldSnapshot,
|
||||||
@@ -12,6 +13,10 @@ import type {
|
|||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
import type { ZoomBlend } from "./viewerConstants";
|
import type { ZoomBlend } from "./viewerConstants";
|
||||||
|
|
||||||
|
export const KILOMETERS_PER_AU = 149_597_870.7;
|
||||||
|
export const DISPLAY_UNITS_PER_KILOMETER = 0.0000015;
|
||||||
|
export const DISPLAY_UNITS_PER_LIGHT_YEAR = 2600;
|
||||||
|
|
||||||
export function formatInventory(entries: { itemId: string; amount: number }[]): string {
|
export function formatInventory(entries: { itemId: string; amount: number }[]): string {
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return "empty";
|
return "empty";
|
||||||
@@ -30,6 +35,61 @@ export function formatVector(vector: Vector3Dto): string {
|
|||||||
return `${vector.x.toFixed(1)}, ${vector.y.toFixed(1)}, ${vector.z.toFixed(1)}`;
|
return `${vector.x.toFixed(1)}, ${vector.y.toFixed(1)}, ${vector.z.toFixed(1)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatNumber(value: number, fractionDigits: number) {
|
||||||
|
return new Intl.NumberFormat("en-US", {
|
||||||
|
minimumFractionDigits: fractionDigits,
|
||||||
|
maximumFractionDigits: fractionDigits,
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatLocalDistance(value: number): string {
|
||||||
|
return `${formatNumber(value, 0)} km`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSystemDistance(value: number): string {
|
||||||
|
return `${formatNumber(value, 2)} AU`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatGalaxyDistance(value: number): string {
|
||||||
|
return `${formatNumber(value, 2)} ly`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAdaptiveDistanceFromKilometers(kilometers: number): string {
|
||||||
|
const absoluteKilometers = Math.max(0, kilometers);
|
||||||
|
const meters = absoluteKilometers * 1000;
|
||||||
|
const astronomicalUnits = absoluteKilometers / KILOMETERS_PER_AU;
|
||||||
|
const lightYears = absoluteKilometers / (KILOMETERS_PER_AU * 63_241.077);
|
||||||
|
|
||||||
|
if (lightYears >= 0.1) {
|
||||||
|
return `${formatNumber(lightYears, 2)} ly`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (astronomicalUnits >= 0.1) {
|
||||||
|
return `${formatNumber(astronomicalUnits, astronomicalUnits >= 10 ? 1 : 3)} AU`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absoluteKilometers >= 1) {
|
||||||
|
return `${formatNumber(absoluteKilometers, absoluteKilometers >= 100 ? 0 : 2)} km`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${formatNumber(meters, meters >= 100 ? 0 : 1)} m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatShipSpeed(ship: ShipSnapshot): string {
|
||||||
|
const speed = Math.max(0, ship.travelSpeed);
|
||||||
|
const unit = ship.travelSpeedUnit;
|
||||||
|
if (unit === "ly/s") {
|
||||||
|
return `${formatNumber(speed, 3)} ly/s`;
|
||||||
|
}
|
||||||
|
if (unit === "AU/s") {
|
||||||
|
return `${formatNumber(speed, 4)} AU/s`;
|
||||||
|
}
|
||||||
|
if (speed >= 1000) {
|
||||||
|
return `${formatNumber(speed / 1000, 2)} km/s`;
|
||||||
|
}
|
||||||
|
return `${formatNumber(speed, 0)} m/s`;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatBytes(bytes: number): string {
|
export function formatBytes(bytes: number): string {
|
||||||
if (bytes >= 1024 * 1024) {
|
if (bytes >= 1024 * 1024) {
|
||||||
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
||||||
@@ -71,6 +131,26 @@ export function toThreeVector(vector: Vector3Dto): THREE.Vector3 {
|
|||||||
return new THREE.Vector3(vector.x, vector.y, vector.z);
|
return new THREE.Vector3(vector.x, vector.y, vector.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function scaleGalaxyScalar(lightYears: number): number {
|
||||||
|
return lightYears * DISPLAY_UNITS_PER_LIGHT_YEAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scaleLocalScalar(kilometers: number): number {
|
||||||
|
return kilometers * DISPLAY_UNITS_PER_KILOMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scaleGalaxyVector(vector: THREE.Vector3): THREE.Vector3 {
|
||||||
|
return vector.clone().multiplyScalar(DISPLAY_UNITS_PER_LIGHT_YEAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDisplayGalaxyVector(vector: Vector3Dto): THREE.Vector3 {
|
||||||
|
return scaleGalaxyVector(toThreeVector(vector));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scaleLocalVector(vector: THREE.Vector3): THREE.Vector3 {
|
||||||
|
return vector.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER);
|
||||||
|
}
|
||||||
|
|
||||||
export function currentWorldTimeSeconds(world: WorldState | undefined, worldTimeSyncMs: number): number {
|
export function currentWorldTimeSeconds(world: WorldState | undefined, worldTimeSyncMs: number): number {
|
||||||
if (!world) {
|
if (!world) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -150,7 +230,7 @@ export function celestialRenderRadius(size: number, scale: number, minRadius: nu
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function computeMoonRenderRadius(planet: PlanetSnapshot, moonIndex: number, seed: number): number {
|
export function computeMoonRenderRadius(planet: PlanetSnapshot, moonIndex: number, seed: number): number {
|
||||||
return celestialRenderRadius(computeMoonSize(planet, moonIndex, seed), MOON_RENDER_SCALE, 2.5, 1.04);
|
return celestialRenderRadius(computeMoonSize(planet, moonIndex, seed), 0.00011, 0.025, 0.62);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function starHaloOpacity(starKind: string): number {
|
export function starHaloOpacity(starKind: string): number {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
ShipVisual,
|
ShipVisual,
|
||||||
SystemVisual,
|
SystemVisual,
|
||||||
WorldState,
|
WorldState,
|
||||||
|
ZoomLevel,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
|
||||||
export interface ViewerNavigationContext {
|
export interface ViewerNavigationContext {
|
||||||
@@ -34,6 +35,7 @@ export interface ViewerNavigationContext {
|
|||||||
getCameraTargetShipId: () => string | undefined;
|
getCameraTargetShipId: () => string | undefined;
|
||||||
setCameraTargetShipId: (value: string | undefined) => void;
|
setCameraTargetShipId: (value: string | undefined) => void;
|
||||||
getCurrentDistance: () => number;
|
getCurrentDistance: () => number;
|
||||||
|
getZoomLevel: () => ZoomLevel;
|
||||||
getSelectedItems: () => Selectable[];
|
getSelectedItems: () => Selectable[];
|
||||||
getOrbitYaw: () => number;
|
getOrbitYaw: () => number;
|
||||||
galaxyFocus: THREE.Vector3;
|
galaxyFocus: THREE.Vector3;
|
||||||
@@ -149,7 +151,7 @@ export class ViewerNavigationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateSystemDetailVisibility() {
|
updateSystemDetailVisibility() {
|
||||||
updateSystemDetailVisibility(this.context.systemVisuals, this.context.getActiveSystemId());
|
updateSystemDetailVisibility(this.context.systemVisuals, this.context.getActiveSystemId(), this.context.getZoomLevel());
|
||||||
}
|
}
|
||||||
|
|
||||||
getCameraFocusWorldPosition() {
|
getCameraFocusWorldPosition() {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { formatInventory, formatVector, inventoryAmount } from "./viewerMath";
|
import {
|
||||||
|
formatInventory,
|
||||||
|
formatLocalDistance,
|
||||||
|
formatShipSpeed,
|
||||||
|
formatSystemDistance,
|
||||||
|
inventoryAmount,
|
||||||
|
} from "./viewerMath";
|
||||||
import { describeOrbitalParent, describeSelectable, describeShipCurrentAction, describeShipState, describeSpatialNodePathWithinSystem, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
|
import { describeOrbitalParent, describeSelectable, describeShipCurrentAction, describeShipState, describeSpatialNodePathWithinSystem, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
|
||||||
import type {
|
import type {
|
||||||
CameraMode,
|
CameraMode,
|
||||||
@@ -127,7 +133,7 @@ export function updateDetailPanel(
|
|||||||
<p>Fuel ${fuelStored.toFixed(1)}<br>Capacitor ${ship.energyStored.toFixed(1)}</p>
|
<p>Fuel ${fuelStored.toFixed(1)}<br>Capacitor ${ship.energyStored.toFixed(1)}</p>
|
||||||
<p>Cargo ${cargoLabel} ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p>
|
<p>Cargo ${cargoLabel} ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p>
|
||||||
<p>Inventory ${formatInventory(ship.inventory)}</p>
|
<p>Inventory ${formatInventory(ship.inventory)}</p>
|
||||||
<p>Velocity ${formatVector(ship.localVelocity)}</p>
|
<p>Speed ${formatShipSpeed(ship)}</p>
|
||||||
<p>Camera ${cameraMode === "follow" && cameraTargetShipId === ship.id ? "camera-follow" : "tactical"}<br>Press C to toggle follow</p>
|
<p>Camera ${cameraMode === "follow" && cameraTargetShipId === ship.id ? "camera-follow" : "tactical"}<br>Press C to toggle follow</p>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
@@ -234,7 +240,7 @@ export function updateDetailPanel(
|
|||||||
detailTitleEl.textContent = `Bubble ${bubble.id}`;
|
detailTitleEl.textContent = `Bubble ${bubble.id}`;
|
||||||
detailBodyEl.innerHTML = `
|
detailBodyEl.innerHTML = `
|
||||||
<p>${bubble.systemId}</p>
|
<p>${bubble.systemId}</p>
|
||||||
<p>Anchor node ${bubble.nodeId}<br>Radius ${bubble.radius.toFixed(0)}</p>
|
<p>Anchor node ${bubble.nodeId}<br>Radius ${formatLocalDistance(bubble.radius)}</p>
|
||||||
<p>Ships ${bubble.occupantShipIds.length}<br>Stations ${bubble.occupantStationIds.length}</p>
|
<p>Ships ${bubble.occupantShipIds.length}<br>Stations ${bubble.occupantStationIds.length}</p>
|
||||||
<p>Claims ${bubble.occupantClaimIds.length}<br>Construction sites ${bubble.occupantConstructionSiteIds.length}</p>
|
<p>Claims ${bubble.occupantClaimIds.length}<br>Construction sites ${bubble.occupantConstructionSiteIds.length}</p>
|
||||||
`;
|
`;
|
||||||
@@ -285,7 +291,7 @@ export function updateDetailPanel(
|
|||||||
<p>${system.label}</p>
|
<p>${system.label}</p>
|
||||||
<p>Parent ${parent}</p>
|
<p>Parent ${parent}</p>
|
||||||
<p>${planet.planetType} · ${planet.shape} · Moons ${planet.moonCount}</p>
|
<p>${planet.planetType} · ${planet.shape} · Moons ${planet.moonCount}</p>
|
||||||
<p>Orbit ${planet.orbitRadius.toFixed(0)}<br>Speed ${planet.orbitSpeed.toFixed(3)}<br>Ecc ${planet.orbitEccentricity.toFixed(3)}<br>Inc ${planet.orbitInclination.toFixed(1)}°</p>
|
<p>Orbit ${formatSystemDistance(planet.orbitRadius)}<br>Speed ${planet.orbitSpeed.toFixed(3)}<br>Ecc ${planet.orbitEccentricity.toFixed(3)}<br>Inc ${planet.orbitInclination.toFixed(1)}°</p>
|
||||||
<p>Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°</p>
|
<p>Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°</p>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { ACTIVE_SYSTEM_DETAIL_SCALE, PROJECTED_GALAXY_RADIUS } from "./viewerConstants";
|
import { ACTIVE_SYSTEM_DETAIL_SCALE, PROJECTED_GALAXY_RADIUS } from "./viewerConstants";
|
||||||
import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeSeconds } from "./viewerMath";
|
import { computeMoonLocalPosition, computePlanetLocalPosition, currentWorldTimeSeconds, scaleLocalVector } from "./viewerMath";
|
||||||
import { rawObject } from "./viewerScenePrimitives";
|
import { rawObject } from "./viewerScenePrimitives";
|
||||||
import type { PlanetVisual, ShipVisual, SystemSummaryVisual, SystemVisual, WorldState } from "./viewerTypes";
|
import type { PlanetVisual, ShipVisual, SystemSummaryVisual, SystemVisual, WorldState } from "./viewerTypes";
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export function updatePlanetPresentation(
|
|||||||
const nowSeconds = currentWorldTimeSeconds(world, worldTimeSyncMs);
|
const nowSeconds = currentWorldTimeSeconds(world, worldTimeSyncMs);
|
||||||
for (const visual of planetVisuals) {
|
for (const visual of planetVisuals) {
|
||||||
const scale = visual.systemId === activeSystemId ? ACTIVE_SYSTEM_DETAIL_SCALE : 1;
|
const scale = visual.systemId === activeSystemId ? ACTIVE_SYSTEM_DETAIL_SCALE : 1;
|
||||||
const localPosition = computePlanetLocalPosition(visual.planet, nowSeconds);
|
const localPosition = scaleLocalVector(computePlanetLocalPosition(visual.planet, nowSeconds));
|
||||||
const orbitOffset = visual.systemId === activeSystemId
|
const orbitOffset = visual.systemId === activeSystemId
|
||||||
? systemFocusLocal.clone().multiplyScalar(-scale)
|
? systemFocusLocal.clone().multiplyScalar(-scale)
|
||||||
: new THREE.Vector3();
|
: new THREE.Vector3();
|
||||||
@@ -54,7 +54,7 @@ export function updatePlanetPresentation(
|
|||||||
moon.orbit.setScaleScalar(scale);
|
moon.orbit.setScaleScalar(scale);
|
||||||
moon.mesh.setPosition(
|
moon.mesh.setPosition(
|
||||||
position.clone().add(
|
position.clone().add(
|
||||||
computeMoonLocalPosition(visual.planet, moonIndex, nowSeconds, world?.seed ?? 1).multiplyScalar(scale),
|
scaleLocalVector(computeMoonLocalPosition(visual.planet, moonIndex, nowSeconds, world?.seed ?? 1)).multiplyScalar(scale),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { computeZoomBlend } from "./viewerMath";
|
|
||||||
import {
|
import {
|
||||||
updateNetworkPanel as renderNetworkPanel,
|
updateNetworkPanel as renderNetworkPanel,
|
||||||
recordPerformanceStats,
|
recordPerformanceStats,
|
||||||
@@ -63,37 +62,32 @@ export class ViewerPresentationController {
|
|||||||
|
|
||||||
applyZoomPresentation() {
|
applyZoomPresentation() {
|
||||||
const activeSystemId = this.context.getActiveSystemId();
|
const activeSystemId = this.context.getActiveSystemId();
|
||||||
const blend = computeZoomBlend(this.context.getCurrentDistance());
|
const zoomLevel = this.context.getZoomLevel();
|
||||||
|
const isUniverse = zoomLevel === "universe";
|
||||||
|
|
||||||
for (const entry of this.context.presentationEntries) {
|
for (const entry of this.context.presentationEntries) {
|
||||||
const systemId = entry.systemId;
|
const systemId = entry.systemId;
|
||||||
const isActiveDetail = !systemId || systemId === activeSystemId;
|
const isActiveDetail = !systemId || systemId === activeSystemId;
|
||||||
const isProjectedSystemIcon = !!activeSystemId
|
|
||||||
&& !!systemId
|
|
||||||
&& systemId !== activeSystemId
|
|
||||||
&& this.context.systemVisuals.get(systemId)?.icon === entry.icon;
|
|
||||||
const detailAlpha = entry.hideDetailInUniverse
|
const detailAlpha = entry.hideDetailInUniverse
|
||||||
? Math.max(blend.localWeight, blend.systemWeight) * (isActiveDetail ? 1 : 0)
|
? (!isUniverse && isActiveDetail ? 1 : 0)
|
||||||
: 1;
|
: 1;
|
||||||
const iconAlpha = isProjectedSystemIcon
|
const iconAlpha = entry.hideIconInUniverse
|
||||||
? 0
|
? (isUniverse ? 1 : 0)
|
||||||
: entry.hideIconInUniverse
|
: (isUniverse ? 1 : 0);
|
||||||
? blend.systemWeight * (isActiveDetail ? 1 : 0)
|
|
||||||
: Math.max(blend.systemWeight, blend.universeWeight);
|
|
||||||
|
|
||||||
entry.detail.setOpacity(detailAlpha);
|
entry.detail.setOpacity(detailAlpha);
|
||||||
entry.icon.setOpacity(iconAlpha);
|
entry.icon.setOpacity(iconAlpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const orbitLine of this.context.orbitLines) {
|
for (const orbitLine of this.context.orbitLines) {
|
||||||
const alpha = this.resolveOrbitLineOpacity(orbitLine, blend, activeSystemId);
|
const alpha = this.resolveOrbitLineOpacity(orbitLine, zoomLevel, activeSystemId);
|
||||||
orbitLine.line.setOpacity(alpha);
|
orbitLine.line.setOpacity(alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [systemId, summaryVisual] of this.context.systemSummaryVisuals.entries()) {
|
for (const [systemId, summaryVisual] of this.context.systemSummaryVisuals.entries()) {
|
||||||
const summaryOpacity = systemId === activeSystemId
|
const summaryOpacity = isUniverse
|
||||||
? 0
|
? 0.96
|
||||||
: (activeSystemId ? 0.72 : 0.96);
|
: 0;
|
||||||
summaryVisual.sprite.setOpacity(summaryOpacity);
|
summaryVisual.sprite.setOpacity(summaryOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,14 +166,14 @@ export class ViewerPresentationController {
|
|||||||
return new THREE.Vector2(clientX - bounds.left, clientY - bounds.top);
|
return new THREE.Vector2(clientX - bounds.left, clientY - bounds.top);
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveOrbitLineOpacity(orbitLine: OrbitLineVisual, blend: ReturnType<typeof computeZoomBlend>, activeSystemId?: string) {
|
private resolveOrbitLineOpacity(orbitLine: OrbitLineVisual, zoomLevel: "local" | "system" | "universe", activeSystemId?: string) {
|
||||||
if (!activeSystemId || orbitLine.systemId !== activeSystemId) {
|
if (zoomLevel === "universe" || !activeSystemId || orbitLine.systemId !== activeSystemId) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = this.context.getSelectedItems();
|
const selected = this.context.getSelectedItems();
|
||||||
const selectedItem = selected.length === 1 ? selected[0] : undefined;
|
const selectedItem = selected.length === 1 ? selected[0] : undefined;
|
||||||
const baseAlpha = Math.max(blend.localWeight * 0.55, blend.systemWeight);
|
const baseAlpha = zoomLevel === "local" ? 0.55 : 0.9;
|
||||||
|
|
||||||
if (selectedItem?.kind === "planet" && selectedItem.systemId === activeSystemId) {
|
if (selectedItem?.kind === "planet" && selectedItem.systemId === activeSystemId) {
|
||||||
return orbitLine.kind === "moon" && orbitLine.planetIndex === selectedItem.planetIndex
|
return orbitLine.kind === "moon" && orbitLine.planetIndex === selectedItem.planetIndex
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export class ViewerSceneDataController {
|
|||||||
createWorldPresentationContext(overrides: {
|
createWorldPresentationContext(overrides: {
|
||||||
world: any;
|
world: any;
|
||||||
activeSystemId?: string;
|
activeSystemId?: string;
|
||||||
|
zoomLevel: any;
|
||||||
orbitYaw: number;
|
orbitYaw: number;
|
||||||
camera: THREE.PerspectiveCamera;
|
camera: THREE.PerspectiveCamera;
|
||||||
systemFocusLocal: THREE.Vector3;
|
systemFocusLocal: THREE.Vector3;
|
||||||
@@ -160,6 +161,7 @@ export class ViewerSceneDataController {
|
|||||||
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
||||||
worldSeed: this.context.getWorldSeed(),
|
worldSeed: this.context.getWorldSeed(),
|
||||||
activeSystemId: overrides.activeSystemId,
|
activeSystemId: overrides.activeSystemId,
|
||||||
|
zoomLevel: overrides.zoomLevel,
|
||||||
orbitYaw: overrides.orbitYaw,
|
orbitYaw: overrides.orbitYaw,
|
||||||
camera: overrides.camera,
|
camera: overrides.camera,
|
||||||
systemFocusLocal: overrides.systemFocusLocal,
|
systemFocusLocal: overrides.systemFocusLocal,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
computeMoonOrbitRadius,
|
computeMoonOrbitRadius,
|
||||||
computeMoonRenderRadius,
|
computeMoonRenderRadius,
|
||||||
computePlanetLocalPosition,
|
computePlanetLocalPosition,
|
||||||
|
scaleLocalScalar,
|
||||||
|
scaleLocalVector,
|
||||||
starHaloOpacity,
|
starHaloOpacity,
|
||||||
toThreeVector,
|
toThreeVector,
|
||||||
} from "./viewerMath";
|
} from "./viewerMath";
|
||||||
@@ -100,7 +102,7 @@ export function createConstructionSiteMesh(site: ConstructionSiteSnapshot): Scen
|
|||||||
|
|
||||||
export function createStarCluster(system: SystemSnapshot): SceneNode {
|
export function createStarCluster(system: SystemSnapshot): SceneNode {
|
||||||
const root = new THREE.Group();
|
const root = new THREE.Group();
|
||||||
const renderedStarSize = celestialRenderRadius(system.starSize, STAR_RENDER_SCALE, 8, 1.02);
|
const renderedStarSize = celestialRenderRadius(system.starSize, 0.00018, 0.16, 0.62);
|
||||||
const offsets = system.starCount > 1
|
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(-renderedStarSize * 0.55, 0, 0), new THREE.Vector3(renderedStarSize * 0.75, renderedStarSize * 0.08, 0)]
|
||||||
: [new THREE.Vector3(0, 0, 0)];
|
: [new THREE.Vector3(0, 0, 0)];
|
||||||
@@ -131,7 +133,7 @@ export function createStarCluster(system: SystemSnapshot): SceneNode {
|
|||||||
export function createPlanetOrbit(planet: PlanetSnapshot): SceneNode {
|
export function createPlanetOrbit(planet: PlanetSnapshot): SceneNode {
|
||||||
const points = Array.from({ length: 120 }, (_, index) => {
|
const points = Array.from({ length: 120 }, (_, index) => {
|
||||||
const phaseDegrees = (index / 120) * 360;
|
const phaseDegrees = (index / 120) * 360;
|
||||||
return computePlanetLocalPosition(planet, 0, phaseDegrees);
|
return scaleLocalVector(computePlanetLocalPosition(planet, 0, phaseDegrees));
|
||||||
});
|
});
|
||||||
|
|
||||||
return createSceneNode(new THREE.LineLoop(
|
return createSceneNode(new THREE.LineLoop(
|
||||||
@@ -141,7 +143,7 @@ export function createPlanetOrbit(planet: PlanetSnapshot): SceneNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createPlanetRing(planet: PlanetSnapshot): SceneNode {
|
export function createPlanetRing(planet: PlanetSnapshot): SceneNode {
|
||||||
const renderedPlanetRadius = celestialRenderRadius(planet.size, PLANET_RENDER_SCALE, 7, 1.06);
|
const renderedPlanetRadius = celestialRenderRadius(planet.size, 0.00012, 0.03, 0.62);
|
||||||
const ring = new THREE.Mesh(
|
const ring = new THREE.Mesh(
|
||||||
new THREE.RingGeometry(renderedPlanetRadius * 1.35, renderedPlanetRadius * 2.15, 48),
|
new THREE.RingGeometry(renderedPlanetRadius * 1.35, renderedPlanetRadius * 2.15, 48),
|
||||||
new THREE.MeshBasicMaterial({
|
new THREE.MeshBasicMaterial({
|
||||||
@@ -161,7 +163,7 @@ export function createMoonVisuals(planet: PlanetSnapshot, seed: number): MoonVis
|
|||||||
const moons: MoonVisual[] = [];
|
const moons: MoonVisual[] = [];
|
||||||
|
|
||||||
for (let moonIndex = 0; moonIndex < moonCount; moonIndex += 1) {
|
for (let moonIndex = 0; moonIndex < moonCount; moonIndex += 1) {
|
||||||
const orbitRadius = computeMoonOrbitRadius(planet, moonIndex, seed);
|
const orbitRadius = scaleLocalScalar(computeMoonOrbitRadius(planet, moonIndex, seed));
|
||||||
const orbit = new THREE.LineLoop(
|
const orbit = new THREE.LineLoop(
|
||||||
new THREE.BufferGeometry().setFromPoints(
|
new THREE.BufferGeometry().setFromPoints(
|
||||||
Array.from({ length: 48 }, (_, index) => {
|
Array.from({ length: 48 }, (_, index) => {
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ import type {
|
|||||||
import {
|
import {
|
||||||
celestialRenderRadius,
|
celestialRenderRadius,
|
||||||
computePlanetLocalPosition,
|
computePlanetLocalPosition,
|
||||||
|
scaleLocalScalar,
|
||||||
|
scaleLocalVector,
|
||||||
|
toDisplayGalaxyVector,
|
||||||
toThreeVector,
|
toThreeVector,
|
||||||
} from "./viewerMath";
|
} from "./viewerMath";
|
||||||
import { getAnimatedShipLocalPosition } from "./viewerPresentation";
|
import { getAnimatedShipLocalPosition } from "./viewerPresentation";
|
||||||
@@ -135,16 +138,16 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
|||||||
|
|
||||||
for (const system of systems) {
|
for (const system of systems) {
|
||||||
const root = createSceneNode(new THREE.Group());
|
const root = createSceneNode(new THREE.Group());
|
||||||
root.setPosition(new THREE.Vector3(system.galaxyPosition.x, system.galaxyPosition.y, system.galaxyPosition.z));
|
root.setPosition(toDisplayGalaxyVector(system.galaxyPosition));
|
||||||
const detailGroup = createSceneNode(new THREE.Group());
|
const detailGroup = createSceneNode(new THREE.Group());
|
||||||
const renderedStarSize = celestialRenderRadius(system.starSize, STAR_RENDER_SCALE, 8, 1.02);
|
const renderedStarSize = celestialRenderRadius(system.starSize, 0.00018, 0.16, 0.62);
|
||||||
|
|
||||||
const starCluster = createStarCluster(system);
|
const starCluster = createStarCluster(system);
|
||||||
const systemIcon = createTacticalIcon(context.documentRef, system.starColor, 96);
|
const systemIcon = createTacticalIcon(context.documentRef, system.starColor, 96);
|
||||||
const shellReticle = createShellReticle(context.documentRef, "#ff3b30", 400);
|
const shellReticle = createShellReticle(context.documentRef, "#ff3b30", 400);
|
||||||
const summaryVisual = createSystemSummaryVisual(
|
const summaryVisual = createSystemSummaryVisual(
|
||||||
context.documentRef,
|
context.documentRef,
|
||||||
new THREE.Vector3(system.galaxyPosition.x, system.galaxyPosition.y + renderedStarSize + 140, system.galaxyPosition.z),
|
toDisplayGalaxyVector(system.galaxyPosition).add(new THREE.Vector3(0, renderedStarSize + 140, 0)),
|
||||||
);
|
);
|
||||||
summaryVisual.sprite.setPosition(new THREE.Vector3(0, renderedStarSize + 110, 0));
|
summaryVisual.sprite.setPosition(new THREE.Vector3(0, renderedStarSize + 110, 0));
|
||||||
root.add(starCluster, systemIcon, shellReticle, summaryVisual.sprite, detailGroup);
|
root.add(starCluster, systemIcon, shellReticle, summaryVisual.sprite, detailGroup);
|
||||||
@@ -157,7 +160,7 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
|||||||
shellReticleBaseScale: 400,
|
shellReticleBaseScale: 400,
|
||||||
detailGroup,
|
detailGroup,
|
||||||
summary: summaryVisual,
|
summary: summaryVisual,
|
||||||
galaxyPosition: toThreeVector(system.galaxyPosition),
|
galaxyPosition: toDisplayGalaxyVector(system.galaxyPosition),
|
||||||
});
|
});
|
||||||
context.systemSummaryVisuals.set(system.id, summaryVisual);
|
context.systemSummaryVisuals.set(system.id, summaryVisual);
|
||||||
registerSelectableDescendants(context.selectableTargets, starCluster, { kind: "system", id: system.id }, (child) => child instanceof THREE.Mesh);
|
registerSelectableDescendants(context.selectableTargets, starCluster, { kind: "system", id: system.id }, (child) => child instanceof THREE.Mesh);
|
||||||
@@ -166,7 +169,7 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
|||||||
|
|
||||||
for (const [planetIndex, planet] of system.planets.entries()) {
|
for (const [planetIndex, planet] of system.planets.entries()) {
|
||||||
const orbit = createPlanetOrbit(planet);
|
const orbit = createPlanetOrbit(planet);
|
||||||
const renderedPlanetRadius = celestialRenderRadius(planet.size, PLANET_RENDER_SCALE, 7, 1.06);
|
const renderedPlanetRadius = celestialRenderRadius(planet.size, 0.00012, 0.03, 0.62);
|
||||||
const planetMesh = createSceneNode(new THREE.Mesh(
|
const planetMesh = createSceneNode(new THREE.Mesh(
|
||||||
new THREE.SphereGeometry(renderedPlanetRadius, 18, 18),
|
new THREE.SphereGeometry(renderedPlanetRadius, 18, 18),
|
||||||
new THREE.MeshStandardMaterial({
|
new THREE.MeshStandardMaterial({
|
||||||
@@ -176,7 +179,7 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho
|
|||||||
emissive: new THREE.Color(planet.color).multiplyScalar(0.04),
|
emissive: new THREE.Color(planet.color).multiplyScalar(0.04),
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
planetMesh.setPosition(computePlanetLocalPosition(planet, worldTimeSeconds));
|
planetMesh.setPosition(scaleLocalVector(computePlanetLocalPosition(planet, worldTimeSeconds)));
|
||||||
const planetIcon = createTacticalIcon(context.documentRef, planet.color, Math.max(24, renderedPlanetRadius * 2));
|
const planetIcon = createTacticalIcon(context.documentRef, planet.color, Math.max(24, renderedPlanetRadius * 2));
|
||||||
planetIcon.setPosition(rawObject(planetMesh).position.clone());
|
planetIcon.setPosition(rawObject(planetMesh).position.clone());
|
||||||
const ring = planet.hasRing ? createPlanetRing(planet) : undefined;
|
const ring = planet.hasRing ? createPlanetRing(planet) : undefined;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
SelectionGroup,
|
SelectionGroup,
|
||||||
WorldState,
|
WorldState,
|
||||||
} from "./viewerTypes";
|
} from "./viewerTypes";
|
||||||
|
import { formatGalaxyDistance } from "./viewerMath";
|
||||||
|
|
||||||
export function describeSelectable(world: WorldState | undefined, item: Selectable): string {
|
export function describeSelectable(world: WorldState | undefined, item: Selectable): string {
|
||||||
if (!world) {
|
if (!world) {
|
||||||
@@ -38,6 +39,83 @@ export function describeSelectable(world: WorldState | undefined, item: Selectab
|
|||||||
return world.systems.get(item.id)?.label ?? item.id;
|
return world.systems.get(item.id)?.label ?? item.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function describeHoverLabel(world: WorldState | undefined, item: Selectable): string | undefined {
|
||||||
|
if (!world) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === "ship") {
|
||||||
|
return world.ships.get(item.id)?.label ?? item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === "station") {
|
||||||
|
return world.stations.get(item.id)?.label ?? item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === "system") {
|
||||||
|
return world.systems.get(item.id)?.label ?? item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === "planet") {
|
||||||
|
const system = world.systems.get(item.systemId);
|
||||||
|
const planet = system?.planets[item.planetIndex];
|
||||||
|
return planet ? `${system?.label ?? item.systemId} / ${planet.label}` : `${item.systemId} / planet ${item.planetIndex + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === "node") {
|
||||||
|
const node = world.nodes.get(item.id);
|
||||||
|
if (!node) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorPath = node.anchorNodeId
|
||||||
|
? describeSpatialNodePathWithinSystem(world, node.systemId, node.anchorNodeId)
|
||||||
|
: undefined;
|
||||||
|
return anchorPath ? `${anchorPath} / ${node.itemId}` : `${node.systemId} / ${node.itemId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === "spatial-node") {
|
||||||
|
const node = world.spatialNodes.get(item.id);
|
||||||
|
if (!node) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === "star") {
|
||||||
|
const system = world.systems.get(node.systemId);
|
||||||
|
return system ? `${system.label} star` : `${node.systemId} star`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return describeSpatialNodePathWithinSystem(world, node.systemId, node.id) ?? `${node.systemId} / ${node.kind}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === "bubble") {
|
||||||
|
const bubble = world.localBubbles.get(item.id);
|
||||||
|
const anchorPath = bubble?.nodeId
|
||||||
|
? describeSpatialNodePathWithinSystem(world, bubble.systemId, bubble.nodeId)
|
||||||
|
: undefined;
|
||||||
|
return anchorPath ? `${anchorPath} bubble` : `Bubble ${item.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === "claim") {
|
||||||
|
const claim = world.claims.get(item.id);
|
||||||
|
const anchorPath = claim?.nodeId
|
||||||
|
? describeSpatialNodePathWithinSystem(world, claim.systemId, claim.nodeId)
|
||||||
|
: undefined;
|
||||||
|
return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.kind === "construction-site") {
|
||||||
|
const site = world.constructionSites.get(item.id);
|
||||||
|
const anchorPath = site?.nodeId
|
||||||
|
? describeSpatialNodePathWithinSystem(world, site.systemId, site.nodeId)
|
||||||
|
: undefined;
|
||||||
|
const siteLabel = site ? (site.blueprintId ?? site.targetDefinitionId) : item.id;
|
||||||
|
return anchorPath ? `${anchorPath} / ${siteLabel}` : `Construction ${siteLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return describeSelectable(world, item);
|
||||||
|
}
|
||||||
|
|
||||||
export function getSelectionGroup(item: Selectable): SelectionGroup {
|
export function getSelectionGroup(item: Selectable): SelectionGroup {
|
||||||
if (item.kind === "ship") {
|
if (item.kind === "ship") {
|
||||||
return "ships";
|
return "ships";
|
||||||
@@ -209,7 +287,7 @@ export function renderSystemDetails(
|
|||||||
<p>Planets ${system.planets.length}<br>Moons ${moonCount}<br>Ships ${shipCount}<br>Stations ${stationCount}</p>
|
<p>Planets ${system.planets.length}<br>Moons ${moonCount}<br>Ships ${shipCount}<br>Stations ${stationCount}</p>
|
||||||
<p>Spatial nodes ${spatialNodeCount}<br>Resource nodes ${nodeCount}<br>Bubbles ${bubbleCount}</p>
|
<p>Spatial nodes ${spatialNodeCount}<br>Resource nodes ${nodeCount}<br>Bubbles ${bubbleCount}</p>
|
||||||
<p>Claims ${claimCount}<br>Construction sites ${constructionCount}</p>
|
<p>Claims ${claimCount}<br>Construction sites ${constructionCount}</p>
|
||||||
<p>Height ${system.galaxyPosition.y.toFixed(0)}</p>
|
<p>Height ${formatGalaxyDistance(system.galaxyPosition.y)}</p>
|
||||||
<p>${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("<br>")}</p>
|
<p>${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("<br>")}</p>
|
||||||
${followText}
|
${followText}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
computePlanetLocalPosition,
|
computePlanetLocalPosition,
|
||||||
currentWorldTimeSeconds,
|
currentWorldTimeSeconds,
|
||||||
resolveOrbitalAnchorPosition,
|
resolveOrbitalAnchorPosition,
|
||||||
|
toDisplayGalaxyVector,
|
||||||
toThreeVector,
|
toThreeVector,
|
||||||
} from "./viewerMath";
|
} from "./viewerMath";
|
||||||
import { describeActiveSpace } from "./viewerSelection";
|
import { describeActiveSpace } from "./viewerSelection";
|
||||||
@@ -20,6 +21,7 @@ import type {
|
|||||||
LocalBubbleSnapshot,
|
LocalBubbleSnapshot,
|
||||||
ResourceNodeDelta,
|
ResourceNodeDelta,
|
||||||
ResourceNodeSnapshot,
|
ResourceNodeSnapshot,
|
||||||
|
ShipSnapshot,
|
||||||
} from "./contracts";
|
} from "./contracts";
|
||||||
import type {
|
import type {
|
||||||
BubbleVisual,
|
BubbleVisual,
|
||||||
@@ -52,6 +54,7 @@ export interface WorldOrbitalContext {
|
|||||||
|
|
||||||
export interface WorldPresentationContext extends WorldOrbitalContext {
|
export interface WorldPresentationContext extends WorldOrbitalContext {
|
||||||
activeSystemId?: string;
|
activeSystemId?: string;
|
||||||
|
zoomLevel: ZoomLevel;
|
||||||
orbitYaw: number;
|
orbitYaw: number;
|
||||||
camera: THREE.PerspectiveCamera;
|
camera: THREE.PerspectiveCamera;
|
||||||
systemFocusLocal: THREE.Vector3;
|
systemFocusLocal: THREE.Vector3;
|
||||||
@@ -79,12 +82,19 @@ export interface GameStatusParams {
|
|||||||
export function updateWorldPresentation(context: WorldPresentationContext) {
|
export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const worldTimeSeconds = currentWorldTimeSeconds(context.world, context.worldTimeSyncMs);
|
const worldTimeSeconds = currentWorldTimeSeconds(context.world, context.worldTimeSyncMs);
|
||||||
|
const renderMode = resolveRenderSpaceMode(context.activeSystemId, context.zoomLevel);
|
||||||
|
|
||||||
|
for (const [shipId, visual] of context.shipVisuals.entries()) {
|
||||||
|
const ship = context.world?.ships.get(shipId);
|
||||||
|
if (!ship) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (const visual of context.shipVisuals.values()) {
|
|
||||||
const worldPosition = getAnimatedShipLocalPosition(visual, now);
|
const worldPosition = getAnimatedShipLocalPosition(visual, now);
|
||||||
visual.mesh.setPosition(context.toDisplayLocalPosition(worldPosition, visual.systemId));
|
const displayPosition = resolveShipWorldPosition(context, ship, visual, worldPosition);
|
||||||
|
visual.mesh.setPosition(displayPosition);
|
||||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||||
const shipVisible = visual.systemId === context.activeSystemId;
|
const shipVisible = isShipVisible(renderMode, context.activeSystemId, ship);
|
||||||
visual.mesh.setVisible(shipVisible);
|
visual.mesh.setVisible(shipVisible);
|
||||||
visual.icon.setVisible(shipVisible && rawObject(visual.icon).visible);
|
visual.icon.setVisible(shipVisible && rawObject(visual.icon).visible);
|
||||||
const desiredHeading = resolveShipHeading(visual, worldPosition, context.orbitYaw);
|
const desiredHeading = resolveShipHeading(visual, worldPosition, context.orbitYaw);
|
||||||
@@ -148,6 +158,49 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
|||||||
updateSystemSummaryPresentation(context.systemSummaryVisuals, context.camera, context.activeSystemId);
|
updateSystemSummaryPresentation(context.systemSummaryVisuals, context.camera, context.activeSystemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RenderSpaceMode = "galaxy" | "system" | "local";
|
||||||
|
|
||||||
|
function resolveRenderSpaceMode(activeSystemId: string | undefined, zoomLevel: ZoomLevel): RenderSpaceMode {
|
||||||
|
if (!activeSystemId || zoomLevel === "universe") {
|
||||||
|
return "galaxy";
|
||||||
|
}
|
||||||
|
|
||||||
|
return zoomLevel === "local" ? "local" : "system";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isShipVisible(mode: RenderSpaceMode, activeSystemId: string | undefined, ship: ShipSnapshot) {
|
||||||
|
if (ship.spatialState.movementRegime === "ftl-transit") {
|
||||||
|
return mode === "galaxy";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activeSystemId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ship.systemId === activeSystemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveShipWorldPosition(
|
||||||
|
context: Pick<WorldPresentationContext, "world" | "toDisplayLocalPosition">,
|
||||||
|
ship: ShipSnapshot,
|
||||||
|
visual: ShipVisual,
|
||||||
|
animatedLocalPosition = getAnimatedShipLocalPosition(visual),
|
||||||
|
) {
|
||||||
|
if (ship.spatialState.movementRegime === "ftl-transit") {
|
||||||
|
const destinationNodeId = ship.spatialState.transit?.destinationNodeId;
|
||||||
|
const destinationNode = destinationNodeId ? context.world?.spatialNodes.get(destinationNodeId) : undefined;
|
||||||
|
const originSystem = context.world?.systems.get(ship.systemId);
|
||||||
|
const destinationSystem = destinationNode ? context.world?.systems.get(destinationNode.systemId) : undefined;
|
||||||
|
if (originSystem && destinationSystem) {
|
||||||
|
const origin = toDisplayGalaxyVector(originSystem.galaxyPosition);
|
||||||
|
const destination = toDisplayGalaxyVector(destinationSystem.galaxyPosition);
|
||||||
|
return origin.lerp(destination, THREE.MathUtils.clamp(ship.spatialState.transit?.progress ?? 0, 0, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.toDisplayLocalPosition(animatedLocalPosition, ship.systemId);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, SystemSummaryVisual>) {
|
export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, SystemSummaryVisual>) {
|
||||||
if (!world) {
|
if (!world) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,53 @@
|
|||||||
{
|
{
|
||||||
"initialStations": [
|
"initialStations": [
|
||||||
{ "constructibleId": "station-core", "systemId": "helios", "planetIndex": 2, "lagrangeSide": -1 }
|
{
|
||||||
|
"constructibleId": "station-core",
|
||||||
|
"systemId": "helios",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"shipFormations": [
|
"shipFormations": [
|
||||||
{ "shipId": "constructor", "count": 1, "center": [45, 0, 20], "systemId": "helios" },
|
{
|
||||||
{ "shipId": "miner", "count": 1, "center": [52, 0, 24], "systemId": "helios" },
|
"shipId": "constructor",
|
||||||
{ "shipId": "gas-miner", "count": 1, "center": [60, 0, 28], "systemId": "helios" }
|
"count": 1,
|
||||||
|
"center": [
|
||||||
|
45,
|
||||||
|
0,
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"systemId": "helios"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "miner",
|
||||||
|
"count": 1,
|
||||||
|
"center": [
|
||||||
|
52,
|
||||||
|
0,
|
||||||
|
24
|
||||||
|
],
|
||||||
|
"systemId": "helios"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "gas-miner",
|
||||||
|
"count": 1,
|
||||||
|
"center": [
|
||||||
|
60,
|
||||||
|
0,
|
||||||
|
28
|
||||||
|
],
|
||||||
|
"systemId": "helios"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "gas-miner",
|
||||||
|
"count": 1,
|
||||||
|
"center": [
|
||||||
|
60,
|
||||||
|
0,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"systemId": "helios"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"patrolRoutes": [],
|
"patrolRoutes": [],
|
||||||
"miningDefaults": {
|
"miningDefaults": {
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
"label": "Vanguard Frigate",
|
"label": "Vanguard Frigate",
|
||||||
"role": "military",
|
"role": "military",
|
||||||
"shipClass": "frigate",
|
"shipClass": "frigate",
|
||||||
"speed": 50,
|
"speed": 120000,
|
||||||
"ftlSpeed": 3200,
|
"warpSpeed": 0.22,
|
||||||
|
"ftlSpeed": 0.75,
|
||||||
"spoolTime": 2.2,
|
"spoolTime": 2.2,
|
||||||
"cargoCapacity": 0,
|
"cargoCapacity": 0,
|
||||||
"color": "#7ed4ff",
|
"color": "#7ed4ff",
|
||||||
@@ -19,8 +20,9 @@
|
|||||||
"label": "Bulwark Destroyer",
|
"label": "Bulwark Destroyer",
|
||||||
"role": "military",
|
"role": "military",
|
||||||
"shipClass": "destroyer",
|
"shipClass": "destroyer",
|
||||||
"speed": 34,
|
"speed": 95000,
|
||||||
"ftlSpeed": 2900,
|
"warpSpeed": 0.18,
|
||||||
|
"ftlSpeed": 0.68,
|
||||||
"spoolTime": 2.8,
|
"spoolTime": 2.8,
|
||||||
"cargoCapacity": 0,
|
"cargoCapacity": 0,
|
||||||
"color": "#ff8f70",
|
"color": "#ff8f70",
|
||||||
@@ -34,8 +36,9 @@
|
|||||||
"label": "Aegis Cruiser",
|
"label": "Aegis Cruiser",
|
||||||
"role": "military",
|
"role": "military",
|
||||||
"shipClass": "cruiser",
|
"shipClass": "cruiser",
|
||||||
"speed": 28,
|
"speed": 85000,
|
||||||
"ftlSpeed": 2750,
|
"warpSpeed": 0.16,
|
||||||
|
"ftlSpeed": 0.62,
|
||||||
"spoolTime": 3.1,
|
"spoolTime": 3.1,
|
||||||
"cargoCapacity": 0,
|
"cargoCapacity": 0,
|
||||||
"color": "#9ec1ff",
|
"color": "#9ec1ff",
|
||||||
@@ -49,8 +52,9 @@
|
|||||||
"label": "Citadel Carrier",
|
"label": "Citadel Carrier",
|
||||||
"role": "military",
|
"role": "military",
|
||||||
"shipClass": "capital",
|
"shipClass": "capital",
|
||||||
"speed": 18,
|
"speed": 60000,
|
||||||
"ftlSpeed": 2500,
|
"warpSpeed": 0.12,
|
||||||
|
"ftlSpeed": 0.5,
|
||||||
"spoolTime": 4.1,
|
"spoolTime": 4.1,
|
||||||
"cargoCapacity": 0,
|
"cargoCapacity": 0,
|
||||||
"color": "#c6f4ff",
|
"color": "#c6f4ff",
|
||||||
@@ -66,8 +70,9 @@
|
|||||||
"label": "Atlas Hauler",
|
"label": "Atlas Hauler",
|
||||||
"role": "transport",
|
"role": "transport",
|
||||||
"shipClass": "industrial",
|
"shipClass": "industrial",
|
||||||
"speed": 22,
|
"speed": 70000,
|
||||||
"ftlSpeed": 2600,
|
"warpSpeed": 0.14,
|
||||||
|
"ftlSpeed": 0.55,
|
||||||
"spoolTime": 3.3,
|
"spoolTime": 3.3,
|
||||||
"cargoCapacity": 180,
|
"cargoCapacity": 180,
|
||||||
"cargoKind": "bulk-liquid",
|
"cargoKind": "bulk-liquid",
|
||||||
@@ -83,8 +88,9 @@
|
|||||||
"label": "Pioneer Constructor",
|
"label": "Pioneer Constructor",
|
||||||
"role": "construction",
|
"role": "construction",
|
||||||
"shipClass": "industrial",
|
"shipClass": "industrial",
|
||||||
"speed": 20,
|
"speed": 65000,
|
||||||
"ftlSpeed": 2200,
|
"warpSpeed": 0.13,
|
||||||
|
"ftlSpeed": 0.48,
|
||||||
"spoolTime": 3.5,
|
"spoolTime": 3.5,
|
||||||
"cargoCapacity": 160,
|
"cargoCapacity": 160,
|
||||||
"cargoKind": "manufactured",
|
"cargoKind": "manufactured",
|
||||||
@@ -100,8 +106,9 @@
|
|||||||
"label": "Prospector Miner",
|
"label": "Prospector Miner",
|
||||||
"role": "mining",
|
"role": "mining",
|
||||||
"shipClass": "industrial",
|
"shipClass": "industrial",
|
||||||
"speed": 26,
|
"speed": 75000,
|
||||||
"ftlSpeed": 2400,
|
"warpSpeed": 0.15,
|
||||||
|
"ftlSpeed": 0.5,
|
||||||
"spoolTime": 3.1,
|
"spoolTime": 3.1,
|
||||||
"cargoCapacity": 120,
|
"cargoCapacity": 120,
|
||||||
"cargoKind": "bulk-solid",
|
"cargoKind": "bulk-solid",
|
||||||
@@ -117,8 +124,9 @@
|
|||||||
"label": "Nimbus Gas Harvester",
|
"label": "Nimbus Gas Harvester",
|
||||||
"role": "mining",
|
"role": "mining",
|
||||||
"shipClass": "industrial",
|
"shipClass": "industrial",
|
||||||
"speed": 24,
|
"speed": 72000,
|
||||||
"ftlSpeed": 2350,
|
"warpSpeed": 0.145,
|
||||||
|
"ftlSpeed": 0.49,
|
||||||
"spoolTime": 3.2,
|
"spoolTime": 3.2,
|
||||||
"cargoCapacity": 120,
|
"cargoCapacity": 120,
|
||||||
"cargoKind": "bulk-gas",
|
"cargoKind": "bulk-gas",
|
||||||
|
|||||||
@@ -5,45 +5,45 @@
|
|||||||
"position": [0, 0, 0],
|
"position": [0, 0, 0],
|
||||||
"starColor": "#ffd27a",
|
"starColor": "#ffd27a",
|
||||||
"starGlow": "#ffb14a",
|
"starGlow": "#ffb14a",
|
||||||
"starSize": 56,
|
"starSize": 720000,
|
||||||
"gravityWellRadius": 210,
|
"gravityWellRadius": 210,
|
||||||
"asteroidField": {
|
"asteroidField": {
|
||||||
"decorationCount": 180,
|
"decorationCount": 180,
|
||||||
"radiusOffset": 330,
|
"radiusOffset": 330000,
|
||||||
"radiusVariance": 90,
|
"radiusVariance": 90000,
|
||||||
"heightVariance": 18
|
"heightVariance": 18000
|
||||||
},
|
},
|
||||||
"resourceNodes": [],
|
"resourceNodes": [],
|
||||||
"planets": [
|
"planets": [
|
||||||
{ "label": "Icarus", "orbitRadius": 180, "orbitSpeed": 0.18, "size": 20, "color": "#d4a373", "tilt": 0.2 },
|
{ "label": "Icarus", "orbitRadius": 0.36, "orbitSpeed": 0.5093, "size": 4200, "color": "#d4a373", "tilt": 0.2 },
|
||||||
{ "label": "Viridia", "orbitRadius": 300, "orbitSpeed": 0.11, "size": 30, "color": "#58a36c", "tilt": -0.4 },
|
{ "label": "Viridia", "orbitRadius": 0.60, "orbitSpeed": 0.2366, "size": 6200, "color": "#58a36c", "tilt": -0.4 },
|
||||||
{ "label": "Aster", "orbitRadius": 460, "orbitSpeed": 0.08, "size": 38, "color": "#6ea7d4", "tilt": 0.3, "hasRing": true },
|
{ "label": "Aster", "orbitRadius": 0.92, "orbitSpeed": 0.1246, "size": 7800, "color": "#6ea7d4", "tilt": 0.3, "hasRing": true },
|
||||||
{ "label": "Noctis", "orbitRadius": 670, "orbitSpeed": 0.05, "size": 50, "color": "#6958a8", "tilt": -0.15 }
|
{ "label": "Noctis", "orbitRadius": 1.34, "orbitSpeed": 0.0710, "size": 11200, "color": "#6958a8", "tilt": -0.15 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "perseus",
|
"id": "perseus",
|
||||||
"label": "Perseus Gate",
|
"label": "Perseus Gate",
|
||||||
"position": [4400, 0, 620],
|
"position": [4.4, 0, 0.62],
|
||||||
"starColor": "#9dc6ff",
|
"starColor": "#9dc6ff",
|
||||||
"starGlow": "#66a0ff",
|
"starGlow": "#66a0ff",
|
||||||
"starSize": 48,
|
"starSize": 930000,
|
||||||
"gravityWellRadius": 230,
|
"gravityWellRadius": 230,
|
||||||
"asteroidField": {
|
"asteroidField": {
|
||||||
"decorationCount": 180,
|
"decorationCount": 180,
|
||||||
"radiusOffset": 330,
|
"radiusOffset": 330000,
|
||||||
"radiusVariance": 90,
|
"radiusVariance": 90000,
|
||||||
"heightVariance": 18
|
"heightVariance": 18000
|
||||||
},
|
},
|
||||||
"resourceNodes": [
|
"resourceNodes": [
|
||||||
{ "angle": 0.45, "radiusOffset": 360, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 },
|
{ "angle": 0.45, "radiusOffset": 180000, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 },
|
||||||
{ "angle": 2.544395102, "radiusOffset": 360, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 },
|
{ "angle": 2.544395102, "radiusOffset": 180000, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 },
|
||||||
{ "angle": 4.638790205, "radiusOffset": 360, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 }
|
{ "angle": 4.638790205, "radiusOffset": 180000, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 }
|
||||||
],
|
],
|
||||||
"planets": [
|
"planets": [
|
||||||
{ "label": "Talos", "orbitRadius": 200, "orbitSpeed": 0.15, "size": 24, "color": "#c48f6a", "tilt": 0.18 },
|
{ "label": "Talos", "orbitRadius": 0.40, "orbitSpeed": 0.4348, "size": 5000, "color": "#c48f6a", "tilt": 0.18 },
|
||||||
{ "label": "Cygnus", "orbitRadius": 360, "orbitSpeed": 0.1, "size": 34, "color": "#4f84c4", "tilt": -0.22, "hasRing": true },
|
{ "label": "Cygnus", "orbitRadius": 0.72, "orbitSpeed": 0.1800, "size": 6900, "color": "#4f84c4", "tilt": -0.22, "hasRing": true },
|
||||||
{ "label": "Rhea", "orbitRadius": 540, "orbitSpeed": 0.07, "size": 44, "color": "#8f8fb0", "tilt": 0.08 }
|
{ "label": "Rhea", "orbitRadius": 1.08, "orbitSpeed": 0.0981, "size": 9600, "color": "#8f8fb0", "tilt": 0.08 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user