diff --git a/apps/backend/Contracts/WorldContracts.Ships.cs b/apps/backend/Contracts/WorldContracts.Ships.cs index c435cd4..81cd359 100644 --- a/apps/backend/Contracts/WorldContracts.Ships.cs +++ b/apps/backend/Contracts/WorldContracts.Ships.cs @@ -22,6 +22,8 @@ public sealed record ShipSnapshot( string? CargoItemId, float WorkerPopulation, float EnergyStored, + float TravelSpeed, + string TravelSpeedUnit, IReadOnlyList Inventory, string FactionId, float Health, @@ -51,6 +53,8 @@ public sealed record ShipDelta( string? CargoItemId, float WorkerPopulation, float EnergyStored, + float TravelSpeed, + string TravelSpeedUnit, IReadOnlyList Inventory, string FactionId, float Health, diff --git a/apps/backend/Data/WorldDefinitions.cs b/apps/backend/Data/WorldDefinitions.cs index 5f37ee6..c35c8a4 100644 --- a/apps/backend/Data/WorldDefinitions.cs +++ b/apps/backend/Data/WorldDefinitions.cs @@ -125,6 +125,7 @@ public sealed class ShipDefinition public required string Role { get; set; } public required string ShipClass { get; set; } public float Speed { get; set; } + public float WarpSpeed { get; set; } public float FtlSpeed { get; set; } public float SpoolTime { get; set; } public float CargoCapacity { get; set; } diff --git a/apps/backend/Simulation/Model/SimulationUnits.cs b/apps/backend/Simulation/Model/SimulationUnits.cs new file mode 100644 index 0000000..60ef493 --- /dev/null +++ b/apps/backend/Simulation/Model/SimulationUnits.cs @@ -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; +} diff --git a/apps/backend/Simulation/ScenarioLoader.Generation.cs b/apps/backend/Simulation/ScenarioLoader.Generation.cs index 9e185a5..ffb7a4a 100644 --- a/apps/backend/Simulation/ScenarioLoader.Generation.cs +++ b/apps/backend/Simulation/ScenarioLoader.Generation.cs @@ -115,9 +115,9 @@ public sealed partial class ScenarioLoader var compactPositions = new[] { new[] { 0f, 0f, 0f }, - new[] { 2600f, 24f, -420f }, - new[] { -2400f, -36f, 560f }, - new[] { 520f, 42f, 2480f }, + new[] { 2.6f, 0.02f, -0.42f }, + new[] { -2.4f, -0.04f, 0.56f }, + new[] { 0.52f, 0.04f, 2.48f }, }; for (var index = 0; index < systems.Count && index < compactPositions.Length; index += 1) @@ -165,9 +165,9 @@ public sealed partial class ScenarioLoader AsteroidField = new AsteroidFieldDefinition { DecorationCount = template.AsteroidField.DecorationCount + ((generatedIndex % 5) * 10), - RadiusOffset = template.AsteroidField.RadiusOffset + ((generatedIndex % 4) * 18f), - RadiusVariance = template.AsteroidField.RadiusVariance + ((generatedIndex % 3) * 12f), - HeightVariance = template.AsteroidField.HeightVariance + ((generatedIndex % 4) * 4f), + RadiusOffset = template.AsteroidField.RadiusOffset + ((generatedIndex % 4) * 18000f), + RadiusVariance = template.AsteroidField.RadiusVariance + ((generatedIndex % 3) * 12000f), + HeightVariance = template.AsteroidField.HeightVariance + ((generatedIndex % 4) * 4000f), }, ResourceNodes = resourceNodes, Planets = planets, @@ -287,14 +287,14 @@ public sealed partial class ScenarioLoader private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt) { const int armCount = 4; - const float baseInnerRadius = 9000f; - const float radiusStep = 540f; + const float baseInnerRadius = 9f; + const float radiusStep = 0.54f; const float armOffset = MathF.PI * 2f / armCount; var armIndex = (generatedIndex + attempt) % armCount; var armDepth = generatedIndex / armCount; - var radius = baseInnerRadius + (armDepth * radiusStep) + Jitter(generatedIndex * 17 + attempt, 0, 900f); - var angle = (armIndex * armOffset) + (radius / 8200f) + Jitter(generatedIndex, 1 + attempt, 0.16f); + var radius = baseInnerRadius + (armDepth * radiusStep) + Jitter(generatedIndex * 17 + attempt, 0, 0.9f); + var angle = (armIndex * armOffset) + (radius / 8.2f) + Jitter(generatedIndex, 1 + attempt, 0.16f); var x = MathF.Cos(angle) * radius; var z = MathF.Sin(angle) * radius * 0.58f; var y = ComputeSystemHeight(radius, generatedIndex, attempt); @@ -304,9 +304,9 @@ public sealed partial class ScenarioLoader private static Vector3 ComputeFallbackGeneratedSystemPosition(int generatedIndex) { 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 radius = fallbackRadius + (generatedIndex / ringCount) * 1800f; + var radius = fallbackRadius + (generatedIndex / ringCount) * 1.8f; return new Vector3( MathF.Cos(angle) * radius, ComputeSystemHeight(radius, generatedIndex, 99), @@ -334,7 +334,7 @@ public sealed partial class ScenarioLoader { SourceKind = "asteroid-belt", 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), AnchorPlanetIndex = ResolveAsteroidAnchorPlanetIndex(planets), OreAmount = oreAmount, @@ -374,7 +374,7 @@ public sealed partial class ScenarioLoader { SourceKind = "gas-cloud", 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), AnchorPlanetIndex = gasAnchorIndex, OreAmount = gasAmount, @@ -415,7 +415,7 @@ public sealed partial class ScenarioLoader { var planetCount = 2 + (int)MathF.Floor(Hash01(generatedIndex, 2) * 7f); var planets = new List(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; for (var index = 0; index < planetCount; index += 1) @@ -437,13 +437,13 @@ public sealed partial class ScenarioLoader Shape = profile.Shape, MoonCount = profile.BaseMoonCount + moonVariance, 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, OrbitInclination = orbitInclination, OrbitLongitudeOfAscendingNode = Hash01(generatedIndex, 120 + index) * 360f, OrbitArgumentOfPeriapsis = Hash01(generatedIndex, 140 + 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, Tilt = -0.45f + (Hash01(generatedIndex, 60 + index) * 0.9f), 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) { - var normalized = MathF.Min(1f, MathF.Max(0f, (radius - 8000f) / 28000f)); - var band = 220f + (normalized * 760f); + var normalized = MathF.Min(1f, MathF.Max(0f, (radius - 8f) / 28f)); + var band = 0.22f + (normalized * 0.76f); return (Hash01(generatedIndex, 100 + salt) * 2f - 1f) * band; } @@ -528,7 +528,7 @@ public sealed partial class ScenarioLoader int BaseMoonCount, bool CanHaveRing) { - public float OrbitGapMax => OrbitGapMin + 44f; + public float OrbitGapMax => OrbitGapMin + MathF.Max(0.12f, OrbitGapMin * 0.45f); } private static SolarSystemDefinition CreateSolSystem() @@ -546,29 +546,29 @@ public sealed partial class ScenarioLoader { Id = "sol", Label = "Sol", - Position = [18200f, 24f, -11800f], + Position = [18.2f, 0.02f, -11.8f], StarKind = "main-sequence", StarCount = 1, StarColor = "#fff1b8", StarGlow = "#ffd35a", - StarSize = 58f, + StarSize = 696340f, GravityWellRadius = 240f, AsteroidField = new AsteroidFieldDefinition { DecorationCount = 240, - RadiusOffset = ScaleSolOrbitRadiusFromAu(2.82f), - RadiusVariance = 180f, - HeightVariance = 22f, + RadiusOffset = 422000000f, + RadiusVariance = 180000000f, + HeightVariance = 22000000f, }, 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 = 1.8f, RadiusOffset = 148f, 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 = 5.1f, RadiusOffset = 164f, 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 = 2.7f, RadiusOffset = 228f, 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 = "asteroid-belt", Angle = 0.2f, RadiusOffset = 126000f, InclinationDegrees = 4f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, + new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 1.8f, RadiusOffset = 148000f, InclinationDegrees = -6f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, + new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 3.5f, RadiusOffset = 138000f, InclinationDegrees = 8f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, + new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 5.1f, RadiusOffset = 164000f, InclinationDegrees = -5f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, + 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 = 228000f, InclinationDegrees = -4f, AnchorPlanetIndex = 5, OreAmount = 1000f, ItemId = "gas", ShardCount = 12 }, + new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 4.8f, RadiusOffset = 186000f, InclinationDegrees = 6f, AnchorPlanetIndex = 6, OreAmount = 1000f, ItemId = "gas", ShardCount = 10 }, ], Planets = [ @@ -605,7 +605,7 @@ public sealed partial class ScenarioLoader PlanetType = planetType, Shape = shape, MoonCount = moonCount, - OrbitRadius = ScaleSolOrbitRadiusFromAu(orbitRadiusAu), + OrbitRadius = orbitRadiusAu, OrbitSpeed = ComputeSolOrbitSpeed(orbitRadiusAu), OrbitEccentricity = orbitEccentricity, OrbitInclination = orbitInclination, @@ -614,9 +614,16 @@ public sealed partial class ScenarioLoader OrbitPhaseAtEpoch = phaseAtEpoch, Size = planetType switch { - "gas-giant" => label == "Saturn" ? 66f : 72f, - "ice-giant" => 48f, - _ => label == "Earth" ? 28f : label == "Mars" ? 22f : label == "Venus" ? 26f : 20f, + "gas-giant" => label == "Saturn" ? 58232f : 69911f, + "ice-giant" => label == "Uranus" ? 25362f : 24622f, + _ => label switch + { + "Mercury" => 2440f, + "Venus" => 6052f, + "Earth" => 6371f, + "Mars" => 3390f, + _ => 5000f, + }, }, Color = color, Tilt = tilt, @@ -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) { const float earthAngularSpeed = 0.11f; diff --git a/apps/backend/Simulation/ScenarioLoader.Spatial.cs b/apps/backend/Simulation/ScenarioLoader.Spatial.cs index ca649d1..995f9b0 100644 --- a/apps/backend/Simulation/ScenarioLoader.Spatial.cs +++ b/apps/backend/Simulation/ScenarioLoader.Spatial.cs @@ -35,7 +35,7 @@ public sealed partial class ScenarioLoader parentNodeId: starNode.Id); var lagrangeNodes = new Dictionary(StringComparer.Ordinal); - foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet.OrbitRadius, planet.Size, planetIndex)) + foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet)) { var lagrangeNode = AddSpatialNode( nodes, @@ -113,39 +113,52 @@ public sealed partial class ScenarioLoader private static IEnumerable EnumeratePlanetLagrangePoints( Vector3 planetPosition, - float orbitRadius, - float planetSize, - int planetIndex) + PlanetDefinition planet) { var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f)); 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; yield return new LagrangePointPlacement("L1", 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( "L4", Add( - planetPosition, - Add( - Scale(radial, offset * MathF.Cos(triangularAngle)), - Scale(tangential, offset * MathF.Sin(triangularAngle))))); + Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)), + Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle)))); yield return new LagrangePointPlacement( "L5", Add( - planetPosition, - Add( - Scale(radial, offset * MathF.Cos(triangularAngle)), - Scale(tangential, -offset * MathF.Sin(triangularAngle))))); + Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)), + Scale(tangential, -orbitRadiusKm * 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 sizeScale = (planetSize * 1.9f) + 10f; - return MathF.Max(22f + (planetIndex * 2f), MathF.Max(orbitalScale, sizeScale)); + var planetMassProxy = EstimatePlanetMassRatio(planet); + var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f)); + 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( @@ -210,7 +223,7 @@ public sealed partial class ScenarioLoader 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( MathF.Cos(definition.Angle) * definition.RadiusOffset, verticalOffset, @@ -227,8 +240,9 @@ public sealed partial class ScenarioLoader private static Vector3 ComputePlanetPosition(PlanetDefinition planet) { var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch); - var x = MathF.Cos(angle) * planet.OrbitRadius; - var z = MathF.Sin(angle) * planet.OrbitRadius; + var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius); + var x = MathF.Cos(angle) * orbitRadiusKm; + var z = MathF.Sin(angle) * orbitRadiusKm; return new Vector3(x, 0f, z); } diff --git a/apps/backend/Simulation/ScenarioLoader.cs b/apps/backend/Simulation/ScenarioLoader.cs index 2e38df5..d948afe 100644 --- a/apps/backend/Simulation/ScenarioLoader.cs +++ b/apps/backend/Simulation/ScenarioLoader.cs @@ -12,7 +12,7 @@ public sealed partial class ScenarioLoader private const float MinimumRefineryOre = 0f; private const float MinimumRefineryStock = 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 PlanetBubbleRadiusPadding = 80f; private const float MoonBubbleRadiusPadding = 40f; @@ -55,24 +55,24 @@ public sealed partial class ScenarioLoader ]; private static readonly StarProfile[] StarProfiles = [ - new("main-sequence", "#ffd27a", "#ffb14a", 54f, 1), - new("blue-white", "#9dc6ff", "#66a0ff", 50f, 1), - new("white-dwarf", "#f1f5ff", "#b8caff", 26f, 1), - new("brown-dwarf", "#b97d56", "#8a5438", 20f, 1), + new("main-sequence", "#ffd27a", "#ffb14a", 696340f, 1), + new("blue-white", "#9dc6ff", "#66a0ff", 930000f, 1), + new("white-dwarf", "#f1f5ff", "#b8caff", 12000f, 1), + new("brown-dwarf", "#b97d56", "#8a5438", 70000f, 1), new("neutron-star", "#d9ebff", "#7ab4ff", 18f, 1), - new("binary-main-sequence", "#ffe09f", "#ffbe6b", 64f, 2), - new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 34f, 2), + new("binary-main-sequence", "#ffe09f", "#ffbe6b", 780000f, 2), + new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f, 2), ]; private static readonly PlanetProfile[] PlanetProfiles = [ - new("barren", "sphere", "#bca48f", 18f, 38f, 0, false), - new("terrestrial", "sphere", "#58a36c", 24f, 46f, 1, false), - new("oceanic", "sphere", "#4f84c4", 26f, 44f, 2, false), - new("desert", "sphere", "#d4a373", 22f, 42f, 0, false), - new("ice", "sphere", "#c8e4ff", 24f, 40f, 1, false), - new("gas-giant", "oblate", "#d9b06f", 52f, 86f, 8, true), - new("ice-giant", "oblate", "#8fc0d8", 44f, 72f, 5, true), - new("lava", "sphere", "#db6846", 20f, 36f, 0, false), + new("barren", "sphere", "#bca48f", 2800f, 0.22f, 0, false), + new("terrestrial", "sphere", "#58a36c", 6400f, 0.28f, 1, false), + new("oceanic", "sphere", "#4f84c4", 7000f, 0.30f, 2, false), + new("desert", "sphere", "#d4a373", 5200f, 0.26f, 0, false), + new("ice", "sphere", "#c8e4ff", 5800f, 0.32f, 1, false), + new("gas-giant", "oblate", "#d9b06f", 45000f, 1.40f, 8, true), + new("ice-giant", "oblate", "#8fc0d8", 25000f, 1.00f, 5, true), + new("lava", "sphere", "#db6846", 3200f, 0.20f, 0, false), ]; private readonly string _dataRoot; diff --git a/apps/backend/Simulation/SimulationEngine.MovementSystem.cs b/apps/backend/Simulation/SimulationEngine.MovementSystem.cs index d3ee35b..b6f1f2e 100644 --- a/apps/backend/Simulation/SimulationEngine.MovementSystem.cs +++ b/apps/backend/Simulation/SimulationEngine.MovementSystem.cs @@ -2,6 +2,18 @@ namespace SpaceGame.Simulation.Api.Simulation; 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) { var task = ship.ControllerTask; @@ -140,7 +152,7 @@ public sealed partial class SimulationEngine ship.ActionTimer = 0f; 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"; } @@ -199,7 +211,7 @@ public sealed partial class SimulationEngine var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null ? 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)); return ship.Position.DistanceTo(targetPosition) <= 18f ? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode) @@ -258,10 +270,11 @@ public sealed partial class SimulationEngine return "none"; } - var totalDistance = MathF.Max(0.001f, ship.Position.DistanceTo(targetPosition)); - ship.Position = ship.Position.MoveToward(targetPosition, ship.Definition.FtlSpeed * deltaSeconds); - transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance)); - return ship.Position.DistanceTo(targetPosition) <= 24f + var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId); + var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId); + var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition)); + transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance)); + return transit.Progress >= 0.999f ? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode) : "none"; } diff --git a/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs b/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs index 390ee9f..97dffbd 100644 --- a/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs +++ b/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs @@ -11,7 +11,7 @@ public sealed partial class SimulationEngine var eccentricAnomaly = meanAnomaly + (eccentricity * MathF.Sin(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 local = new Vector3( semiMajorAxis * (MathF.Cos(eccentricAnomaly) - eccentricity), @@ -58,7 +58,7 @@ public sealed partial class SimulationEngine private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node) { 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) @@ -73,39 +73,51 @@ public sealed partial class SimulationEngine private static IEnumerable EnumeratePlanetLagrangePoints( Vector3 planetPosition, - float orbitRadius, - float planetSize, - int planetIndex) + PlanetDefinition planet) { var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f)); 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; yield return new LagrangePointPlacement("L1", 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( "L4", Add( - planetPosition, - Add( - Scale(radial, offset * MathF.Cos(triangularAngle)), - Scale(tangential, offset * MathF.Sin(triangularAngle))))); + Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)), + Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle)))); yield return new LagrangePointPlacement( "L5", Add( - planetPosition, - Add( - Scale(radial, offset * MathF.Cos(triangularAngle)), - Scale(tangential, -offset * MathF.Sin(triangularAngle))))); + Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)), + Scale(tangential, -orbitRadiusKm * 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 sizeScale = (planetSize * 1.9f) + 10f; - return MathF.Max(22f + (planetIndex * 2f), MathF.Max(orbitalScale, sizeScale)); + var planetMassProxy = EstimatePlanetMassRatio(planet); + var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f)); + 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) @@ -185,7 +197,7 @@ public sealed partial class SimulationEngine var planetPosition = ComputePlanetPosition(planet, worldTimeSeconds); 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()}"; if (spatialNodesById.TryGetValue(lagrangeId, out var lagrangeNode)) diff --git a/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs b/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs index 25b2dc6..6264e98 100644 --- a/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs +++ b/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs @@ -260,7 +260,9 @@ public sealed partial class SimulationEngine { var destinationEntryNode = ResolveSystemEntryNode(world, toSystemId); 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); return EstimateTimedEnergyUse(world, ship.Definition.SpoolTime, world.Balance.Energy.IdleDrain) + EstimateTimedEnergyUse(world, ftlDuration, world.Balance.Energy.WarpDrain) @@ -278,14 +280,14 @@ public sealed partial class SimulationEngine 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); } 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) + EstimateTimedEnergyUse(world, warpDuration, world.Balance.Energy.WarpDrain); } diff --git a/apps/backend/Simulation/SimulationEngine.Replication.cs b/apps/backend/Simulation/SimulationEngine.Replication.cs index 561fe05..7892253 100644 --- a/apps/backend/Simulation/SimulationEngine.Replication.cs +++ b/apps/backend/Simulation/SimulationEngine.Replication.cs @@ -165,6 +165,8 @@ public sealed partial class SimulationEngine ship.CargoItemId, ship.WorkerPopulation, ship.EnergyStored, + ship.TravelSpeed, + ship.TravelSpeedUnit, ship.Inventory, ship.FactionId, ship.Health, @@ -670,6 +672,8 @@ public sealed partial class SimulationEngine ship.Definition.CargoItemId, ship.WorkerPopulation, ship.EnergyStored, + ToShipTravelSpeed(ship).Speed, + ToShipTravelSpeed(ship).Unit, ToInventoryEntries(ship.Inventory), ship.FactionId, ship.Health, @@ -712,6 +716,16 @@ public sealed partial class SimulationEngine 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) => new(label, Math.Clamp(elapsedSeconds / requiredSeconds, 0f, 1f)); diff --git a/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs b/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs index de63b0c..e38fd55 100644 --- a/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs +++ b/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs @@ -64,7 +64,7 @@ public sealed partial class SimulationEngine } 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"; } @@ -123,7 +123,7 @@ public sealed partial class SimulationEngine var waitDistance = ship.Position.DistanceTo(ship.TargetPosition); 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"; @@ -144,7 +144,7 @@ public sealed partial class SimulationEngine } 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"; } @@ -288,7 +288,7 @@ public sealed partial class SimulationEngine { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds); + ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); return "none"; } @@ -340,7 +340,7 @@ public sealed partial class SimulationEngine { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds); + ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); return "none"; } @@ -405,7 +405,7 @@ public sealed partial class SimulationEngine { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds); + ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); return "none"; } @@ -476,7 +476,7 @@ public sealed partial class SimulationEngine { ship.State = ShipState.LocalFlight; ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, ship.Definition.Speed * deltaSeconds); + ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); return "none"; } diff --git a/apps/viewer/src/ViewerAppController.ts b/apps/viewer/src/ViewerAppController.ts index afad471..335d1d2 100644 --- a/apps/viewer/src/ViewerAppController.ts +++ b/apps/viewer/src/ViewerAppController.ts @@ -263,6 +263,7 @@ export class ViewerAppController { return this.sceneDataController.createWorldPresentationContext({ world: this.world, activeSystemId: this.activeSystemId, + zoomLevel: this.zoomLevel, orbitYaw: this.orbitYaw, camera: this.camera, systemFocusLocal: this.systemFocusLocal, @@ -337,6 +338,7 @@ export class ViewerAppController { this.keyState, this.orbitYaw, this.currentDistance, + this.zoomLevel, this.activeSystemId, this.systemFocusLocal, this.galaxyFocus, diff --git a/apps/viewer/src/contractsShips.ts b/apps/viewer/src/contractsShips.ts index 054ae87..883e97c 100644 --- a/apps/viewer/src/contractsShips.ts +++ b/apps/viewer/src/contractsShips.ts @@ -22,6 +22,8 @@ export interface ShipSnapshot { cargoItemId?: string | null; workerPopulation: number; energyStored: number; + travelSpeed: number; + travelSpeedUnit: string; inventory: InventoryEntry[]; factionId: string; health: number; diff --git a/apps/viewer/src/style.css b/apps/viewer/src/style.css index 884519c..4910035 100644 --- a/apps/viewer/src/style.css +++ b/apps/viewer/src/style.css @@ -68,14 +68,14 @@ canvas { .hover-label { position: absolute; padding: 8px 10px; - border-radius: 999px; + border-radius: 14px; background: rgba(7, 15, 28, 0.88); border: 1px solid rgba(255, 88, 72, 0.5); color: #fff2ef; font-family: "IBM Plex Mono", "SFMono-Regular", monospace; font-size: 0.75rem; - line-height: 1; - white-space: nowrap; + line-height: 1.35; + white-space: pre-line; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32); } diff --git a/apps/viewer/src/viewerCamera.ts b/apps/viewer/src/viewerCamera.ts index a19d74b..1be3f2d 100644 --- a/apps/viewer/src/viewerCamera.ts +++ b/apps/viewer/src/viewerCamera.ts @@ -1,6 +1,6 @@ import * as THREE from "three"; 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 type { BubbleVisual, @@ -13,6 +13,7 @@ import type { SpatialNodeVisual, StructureVisual, WorldState, + ZoomLevel, } from "./viewerTypes"; interface ResolveSelectionPositionParams { @@ -75,6 +76,7 @@ export function updatePanFromKeyboard( keyState: Set, orbitYaw: number, currentDistance: number, + zoomLevel: ZoomLevel, activeSystemId: string | undefined, systemFocusLocal: 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 right = new THREE.Vector3(-forward.z, 0, forward.x); const pan = right.multiplyScalar(move.x).add(forward.multiplyScalar(move.z)); - const speed = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 320, 6800); 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; } + const speed = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 320, 6800); galaxyFocus.addScaledVector(pan, speed * delta); } @@ -127,7 +132,14 @@ export function determineActiveSystemId(params: DetermineActiveSystemParams): st } 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) { @@ -152,7 +164,7 @@ export function determineActiveSystemId(params: DetermineActiveSystemParams): st let nearestSystemId: string | undefined; let nearestDistance = Number.POSITIVE_INFINITY; for (const system of world.systems.values()) { - const center = toThreeVector(system.galaxyPosition); + const center = scaleGalaxyVector(toThreeVector(system.galaxyPosition)); const distance = center.distanceTo(galaxyFocus); if (distance < nearestDistance) { nearestDistance = distance; @@ -222,7 +234,7 @@ export function resolveSelectionPosition(params: ResolveSelectionPositionParams) } 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) { @@ -249,7 +261,7 @@ export function focusOnSelection(params: FocusOnSelectionParams) { if (selectionSystemId && world) { const system = world.systems.get(selectionSystemId); if (system) { - galaxyFocus.copy(toThreeVector(system.galaxyPosition)); + galaxyFocus.copy(scaleGalaxyVector(toThreeVector(system.galaxyPosition))); systemFocusLocal.copy(nextFocus); return; } @@ -325,8 +337,8 @@ export function getCameraFocusWorldPosition(params: CameraFocusParams): THREE.Ve const system = world.systems.get(activeSystemId); return system - ? toThreeVector(system.galaxyPosition).add( - systemFocusLocal.clone().multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE * GALAXY_PARALLAX_FACTOR), + ? scaleGalaxyVector(toThreeVector(system.galaxyPosition)).add( + scaleLocalVector(systemFocusLocal).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE * GALAXY_PARALLAX_FACTOR), ) : galaxyFocus; } @@ -341,18 +353,20 @@ export function toDisplayLocalPosition(params: DisplayLocalPositionParams): THRE } = params; if (!world || !systemId) { - return localPosition.clone(); + return scaleLocalVector(localPosition); } const system = world.systems.get(systemId); 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) { - 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)); } diff --git a/apps/viewer/src/viewerConstants.ts b/apps/viewer/src/viewerConstants.ts index 9d8e464..f4bd66c 100644 --- a/apps/viewer/src/viewerConstants.ts +++ b/apps/viewer/src/viewerConstants.ts @@ -1,9 +1,9 @@ import type { ZoomLevel } from "./viewerTypes"; export const ZOOM_DISTANCE: Record = { - local: 900, + local: 18, system: 3200, - universe: 26000, + universe: 32000, }; 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 PLANET_RENDER_SCALE = 0.95; export const MOON_RENDER_SCALE = 1.1; -export const MIN_CAMERA_DISTANCE = 450; -export const MAX_CAMERA_DISTANCE = 42000; +export const MIN_CAMERA_DISTANCE = 2; +export const MAX_CAMERA_DISTANCE = 52000; export interface ZoomBlend { localWeight: number; diff --git a/apps/viewer/src/viewerControllerFactory.ts b/apps/viewer/src/viewerControllerFactory.ts index e59c9db..a9547c8 100644 --- a/apps/viewer/src/viewerControllerFactory.ts +++ b/apps/viewer/src/viewerControllerFactory.ts @@ -54,6 +54,7 @@ export function createViewerControllers(host: any) { host.cameraTargetShipId = value; }, getCurrentDistance: () => host.currentDistance, + getZoomLevel: () => host.zoomLevel, getSelectedItems: () => host.selectedItems, getOrbitYaw: () => host.orbitYaw, galaxyFocus: host.galaxyFocus, @@ -199,6 +200,7 @@ export function createViewerControllers(host: any) { keyState: host.keyState, getWorld: () => host.world, getActiveSystemId: () => host.activeSystemId, + getZoomLevel: () => host.zoomLevel, getSelectedItems: () => host.selectedItems, setSelectedItems: (items) => { host.selectedItems = items; diff --git a/apps/viewer/src/viewerControls.ts b/apps/viewer/src/viewerControls.ts index 56f0b5d..6c5bb3c 100644 --- a/apps/viewer/src/viewerControls.ts +++ b/apps/viewer/src/viewerControls.ts @@ -1,12 +1,15 @@ import * as THREE from "three"; import { MAX_CAMERA_DISTANCE, MIN_CAMERA_DISTANCE, ZOOM_DISTANCE } from "./viewerConstants"; +import { scaleGalaxyVector, toDisplayGalaxyVector, toThreeVector } from "./viewerMath"; import { rawObject } from "./viewerScenePrimitives"; +import { resolveShipWorldPosition } from "./viewerWorldPresentation"; import type { CameraMode, Selectable, ShipVisual, SystemVisual, WorldState, + ZoomLevel, } from "./viewerTypes"; export function syncFollowStateFromSelection( @@ -129,10 +132,32 @@ export function updateFollowCamera(params: { } const shipLocalPosition = getAnimatedShipLocalPosition(visual); - const shipWorldPosition = toDisplayLocalPosition(shipLocalPosition, ship.systemId); - systemFocusLocal.lerp(shipLocalPosition, 1 - Math.exp(-delta * 8)); + const shipWorldPosition = resolveShipWorldPosition( + { + 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.normalize(); @@ -165,9 +190,10 @@ export function updateFollowCamera(params: { }; } -export function updateSystemDetailVisibility(systemVisuals: Map, activeSystemId?: string) { +export function updateSystemDetailVisibility(systemVisuals: Map, activeSystemId?: string, zoomLevel?: ZoomLevel) { + const detailVisible = !!activeSystemId && zoomLevel !== "universe"; for (const [systemId, visual] of systemVisuals.entries()) { - visual.detailGroup.setVisible(systemId === activeSystemId); + visual.detailGroup.setVisible(detailVisible && systemId === activeSystemId); } } diff --git a/apps/viewer/src/viewerInteraction.ts b/apps/viewer/src/viewerInteraction.ts index 0e0369a..515652b 100644 --- a/apps/viewer/src/viewerInteraction.ts +++ b/apps/viewer/src/viewerInteraction.ts @@ -1,6 +1,13 @@ import * as THREE from "three"; -import { getSelectionGroup } from "./viewerSelection"; -import type { Selectable, SelectionGroup, WorldState } from "./viewerTypes"; +import { describeHoverLabel, getSelectionGroup } from "./viewerSelection"; +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( renderer: THREE.WebGLRenderer, @@ -11,29 +18,49 @@ export function pickSelectableAtClientPosition( clientX: 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, + clientX: number, + clientY: number, +): HoverPickResult | undefined { const bounds = renderer.domElement.getBoundingClientRect(); mouse.x = ((clientX - bounds.left) / bounds.width) * 2 - 1; mouse.y = -(((clientY - bounds.top) / bounds.height) * 2 - 1); raycaster.setFromCamera(mouse, camera); 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: { dragMode?: string; hoverLabelEl: HTMLDivElement; - selection: Selectable | undefined; + hoverPick: HoverPickResult | undefined; activeSystemId?: string; + zoomLevel: ZoomLevel; world?: WorldState; point: THREE.Vector2; + camera: THREE.Camera; }) { const { dragMode, hoverLabelEl, - selection, + hoverPick, activeSystemId, + zoomLevel, world, point, + camera, } = params; if (dragMode) { @@ -41,23 +68,60 @@ export function updateHoverLabel(params: { return; } - if (!selection || selection.kind !== "system" || selection.id === activeSystemId) { + if (!hoverPick) { hoverLabelEl.hidden = true; return; } - const system = world?.systems.get(selection.id); - if (!system) { + const { selection, object } = hoverPick; + const label = describeHoverLabel(world, selection); + if (!label) { hoverLabelEl.hidden = true; return; } + const distance = formatHoverDistance(camera, object, selection, zoomLevel, activeSystemId); + hoverLabelEl.hidden = false; - hoverLabelEl.textContent = system.label; + hoverLabelEl.textContent = `${label}\n${distance}`; hoverLabelEl.style.left = `${point.x + 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( marqueeEl: HTMLDivElement, dragStart: THREE.Vector2, diff --git a/apps/viewer/src/viewerInteractionController.ts b/apps/viewer/src/viewerInteractionController.ts index 937527b..6d87631 100644 --- a/apps/viewer/src/viewerInteractionController.ts +++ b/apps/viewer/src/viewerInteractionController.ts @@ -2,6 +2,7 @@ import * as THREE from "three"; import { completeMarqueeSelection, hideMarqueeBox, + pickSelectableHitAtClientPosition, pickSelectableAtClientPosition, updateHoverLabel, updateMarqueeBox, @@ -17,6 +18,7 @@ import type { DragMode, Selectable, WorldState, + ZoomLevel, } from "./viewerTypes"; export interface ViewerInteractionContext { @@ -30,6 +32,7 @@ export interface ViewerInteractionContext { keyState: Set; getWorld: () => WorldState | undefined; getActiveSystemId: () => string | undefined; + getZoomLevel: () => ZoomLevel; getSelectedItems: () => Selectable[]; setSelectedItems: (items: Selectable[]) => void; getDragMode: () => DragMode | undefined; @@ -247,10 +250,12 @@ export class ViewerInteractionController { updateHoverLabel({ dragMode: this.context.getDragMode(), hoverLabelEl: this.context.hoverLabelEl, - selection: this.pickSelectableAtClientPosition(event.clientX, event.clientY), + hoverPick: this.pickSelectableHitAtClientPosition(event.clientX, event.clientY), activeSystemId: this.context.getActiveSystemId(), + zoomLevel: this.context.getZoomLevel(), world: this.context.getWorld(), 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() { const selection = completeMarqueeSelection({ renderer: this.context.renderer, diff --git a/apps/viewer/src/viewerMath.ts b/apps/viewer/src/viewerMath.ts index 63cbb76..d6edc84 100644 --- a/apps/viewer/src/viewerMath.ts +++ b/apps/viewer/src/viewerMath.ts @@ -1,6 +1,7 @@ import * as THREE from "three"; import { MOON_RENDER_SCALE } from "./viewerConstants"; import type { + ShipSnapshot, PlanetSnapshot, Vector3Dto, WorldSnapshot, @@ -12,6 +13,10 @@ import type { } from "./viewerTypes"; 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 { if (entries.length === 0) { 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)}`; } +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 { if (bytes >= 1024 * 1024) { 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); } +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 { if (!world) { 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 { - 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 { diff --git a/apps/viewer/src/viewerNavigationController.ts b/apps/viewer/src/viewerNavigationController.ts index 94178c6..2e223da 100644 --- a/apps/viewer/src/viewerNavigationController.ts +++ b/apps/viewer/src/viewerNavigationController.ts @@ -22,6 +22,7 @@ import type { ShipVisual, SystemVisual, WorldState, + ZoomLevel, } from "./viewerTypes"; export interface ViewerNavigationContext { @@ -34,6 +35,7 @@ export interface ViewerNavigationContext { getCameraTargetShipId: () => string | undefined; setCameraTargetShipId: (value: string | undefined) => void; getCurrentDistance: () => number; + getZoomLevel: () => ZoomLevel; getSelectedItems: () => Selectable[]; getOrbitYaw: () => number; galaxyFocus: THREE.Vector3; @@ -149,7 +151,7 @@ export class ViewerNavigationController { } updateSystemDetailVisibility() { - updateSystemDetailVisibility(this.context.systemVisuals, this.context.getActiveSystemId()); + updateSystemDetailVisibility(this.context.systemVisuals, this.context.getActiveSystemId(), this.context.getZoomLevel()); } getCameraFocusWorldPosition() { diff --git a/apps/viewer/src/viewerPanels.ts b/apps/viewer/src/viewerPanels.ts index aa3254b..db9692e 100644 --- a/apps/viewer/src/viewerPanels.ts +++ b/apps/viewer/src/viewerPanels.ts @@ -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 type { CameraMode, @@ -127,7 +133,7 @@ export function updateDetailPanel(

Fuel ${fuelStored.toFixed(1)}
Capacitor ${ship.energyStored.toFixed(1)}

Cargo ${cargoLabel} ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}

Inventory ${formatInventory(ship.inventory)}

-

Velocity ${formatVector(ship.localVelocity)}

+

Speed ${formatShipSpeed(ship)}

Camera ${cameraMode === "follow" && cameraTargetShipId === ship.id ? "camera-follow" : "tactical"}
Press C to toggle follow

`; return; @@ -234,7 +240,7 @@ export function updateDetailPanel( detailTitleEl.textContent = `Bubble ${bubble.id}`; detailBodyEl.innerHTML = `

${bubble.systemId}

-

Anchor node ${bubble.nodeId}
Radius ${bubble.radius.toFixed(0)}

+

Anchor node ${bubble.nodeId}
Radius ${formatLocalDistance(bubble.radius)}

Ships ${bubble.occupantShipIds.length}
Stations ${bubble.occupantStationIds.length}

Claims ${bubble.occupantClaimIds.length}
Construction sites ${bubble.occupantConstructionSiteIds.length}

`; @@ -285,7 +291,7 @@ export function updateDetailPanel(

${system.label}

Parent ${parent}

${planet.planetType} · ${planet.shape} · Moons ${planet.moonCount}

-

Orbit ${planet.orbitRadius.toFixed(0)}
Speed ${planet.orbitSpeed.toFixed(3)}
Ecc ${planet.orbitEccentricity.toFixed(3)}
Inc ${planet.orbitInclination.toFixed(1)}°

+

Orbit ${formatSystemDistance(planet.orbitRadius)}
Speed ${planet.orbitSpeed.toFixed(3)}
Ecc ${planet.orbitEccentricity.toFixed(3)}
Inc ${planet.orbitInclination.toFixed(1)}°

Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°

`; return; diff --git a/apps/viewer/src/viewerPresentation.ts b/apps/viewer/src/viewerPresentation.ts index 8e61b69..213cd8c 100644 --- a/apps/viewer/src/viewerPresentation.ts +++ b/apps/viewer/src/viewerPresentation.ts @@ -1,6 +1,6 @@ import * as THREE from "three"; 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 type { PlanetVisual, ShipVisual, SystemSummaryVisual, SystemVisual, WorldState } from "./viewerTypes"; @@ -33,7 +33,7 @@ export function updatePlanetPresentation( const nowSeconds = currentWorldTimeSeconds(world, worldTimeSyncMs); for (const visual of planetVisuals) { 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 ? systemFocusLocal.clone().multiplyScalar(-scale) : new THREE.Vector3(); @@ -54,7 +54,7 @@ export function updatePlanetPresentation( moon.orbit.setScaleScalar(scale); moon.mesh.setPosition( position.clone().add( - computeMoonLocalPosition(visual.planet, moonIndex, nowSeconds, world?.seed ?? 1).multiplyScalar(scale), + scaleLocalVector(computeMoonLocalPosition(visual.planet, moonIndex, nowSeconds, world?.seed ?? 1)).multiplyScalar(scale), ), ); } diff --git a/apps/viewer/src/viewerPresentationController.ts b/apps/viewer/src/viewerPresentationController.ts index 7eb548e..e8a78e7 100644 --- a/apps/viewer/src/viewerPresentationController.ts +++ b/apps/viewer/src/viewerPresentationController.ts @@ -1,5 +1,4 @@ import * as THREE from "three"; -import { computeZoomBlend } from "./viewerMath"; import { updateNetworkPanel as renderNetworkPanel, recordPerformanceStats, @@ -63,37 +62,32 @@ export class ViewerPresentationController { applyZoomPresentation() { 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) { const systemId = entry.systemId; const isActiveDetail = !systemId || systemId === activeSystemId; - const isProjectedSystemIcon = !!activeSystemId - && !!systemId - && systemId !== activeSystemId - && this.context.systemVisuals.get(systemId)?.icon === entry.icon; const detailAlpha = entry.hideDetailInUniverse - ? Math.max(blend.localWeight, blend.systemWeight) * (isActiveDetail ? 1 : 0) + ? (!isUniverse && isActiveDetail ? 1 : 0) : 1; - const iconAlpha = isProjectedSystemIcon - ? 0 - : entry.hideIconInUniverse - ? blend.systemWeight * (isActiveDetail ? 1 : 0) - : Math.max(blend.systemWeight, blend.universeWeight); + const iconAlpha = entry.hideIconInUniverse + ? (isUniverse ? 1 : 0) + : (isUniverse ? 1 : 0); entry.detail.setOpacity(detailAlpha); entry.icon.setOpacity(iconAlpha); } 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); } for (const [systemId, summaryVisual] of this.context.systemSummaryVisuals.entries()) { - const summaryOpacity = systemId === activeSystemId - ? 0 - : (activeSystemId ? 0.72 : 0.96); + const summaryOpacity = isUniverse + ? 0.96 + : 0; summaryVisual.sprite.setOpacity(summaryOpacity); } @@ -172,14 +166,14 @@ export class ViewerPresentationController { return new THREE.Vector2(clientX - bounds.left, clientY - bounds.top); } - private resolveOrbitLineOpacity(orbitLine: OrbitLineVisual, blend: ReturnType, activeSystemId?: string) { - if (!activeSystemId || orbitLine.systemId !== activeSystemId) { + private resolveOrbitLineOpacity(orbitLine: OrbitLineVisual, zoomLevel: "local" | "system" | "universe", activeSystemId?: string) { + if (zoomLevel === "universe" || !activeSystemId || orbitLine.systemId !== activeSystemId) { return 0; } const selected = this.context.getSelectedItems(); 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) { return orbitLine.kind === "moon" && orbitLine.planetIndex === selectedItem.planetIndex diff --git a/apps/viewer/src/viewerSceneDataController.ts b/apps/viewer/src/viewerSceneDataController.ts index e9c23c7..5a5988c 100644 --- a/apps/viewer/src/viewerSceneDataController.ts +++ b/apps/viewer/src/viewerSceneDataController.ts @@ -148,6 +148,7 @@ export class ViewerSceneDataController { createWorldPresentationContext(overrides: { world: any; activeSystemId?: string; + zoomLevel: any; orbitYaw: number; camera: THREE.PerspectiveCamera; systemFocusLocal: THREE.Vector3; @@ -160,6 +161,7 @@ export class ViewerSceneDataController { worldTimeSyncMs: this.context.getWorldTimeSyncMs(), worldSeed: this.context.getWorldSeed(), activeSystemId: overrides.activeSystemId, + zoomLevel: overrides.zoomLevel, orbitYaw: overrides.orbitYaw, camera: overrides.camera, systemFocusLocal: overrides.systemFocusLocal, diff --git a/apps/viewer/src/viewerSceneFactory.ts b/apps/viewer/src/viewerSceneFactory.ts index a8b9e92..ab566bc 100644 --- a/apps/viewer/src/viewerSceneFactory.ts +++ b/apps/viewer/src/viewerSceneFactory.ts @@ -21,6 +21,8 @@ import { computeMoonOrbitRadius, computeMoonRenderRadius, computePlanetLocalPosition, + scaleLocalScalar, + scaleLocalVector, starHaloOpacity, toThreeVector, } from "./viewerMath"; @@ -100,7 +102,7 @@ export function createConstructionSiteMesh(site: ConstructionSiteSnapshot): Scen export function createStarCluster(system: SystemSnapshot): SceneNode { 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 ? [new THREE.Vector3(-renderedStarSize * 0.55, 0, 0), new THREE.Vector3(renderedStarSize * 0.75, renderedStarSize * 0.08, 0)] : [new THREE.Vector3(0, 0, 0)]; @@ -131,7 +133,7 @@ export function createStarCluster(system: SystemSnapshot): SceneNode { export function createPlanetOrbit(planet: PlanetSnapshot): SceneNode { const points = Array.from({ length: 120 }, (_, index) => { const phaseDegrees = (index / 120) * 360; - return computePlanetLocalPosition(planet, 0, phaseDegrees); + return scaleLocalVector(computePlanetLocalPosition(planet, 0, phaseDegrees)); }); return createSceneNode(new THREE.LineLoop( @@ -141,7 +143,7 @@ export function createPlanetOrbit(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( new THREE.RingGeometry(renderedPlanetRadius * 1.35, renderedPlanetRadius * 2.15, 48), new THREE.MeshBasicMaterial({ @@ -161,7 +163,7 @@ export function createMoonVisuals(planet: PlanetSnapshot, seed: number): MoonVis const moons: MoonVisual[] = []; 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( new THREE.BufferGeometry().setFromPoints( Array.from({ length: 48 }, (_, index) => { diff --git a/apps/viewer/src/viewerSceneSync.ts b/apps/viewer/src/viewerSceneSync.ts index 518bd0b..8dac432 100644 --- a/apps/viewer/src/viewerSceneSync.ts +++ b/apps/viewer/src/viewerSceneSync.ts @@ -38,6 +38,9 @@ import type { import { celestialRenderRadius, computePlanetLocalPosition, + scaleLocalScalar, + scaleLocalVector, + toDisplayGalaxyVector, toThreeVector, } from "./viewerMath"; import { getAnimatedShipLocalPosition } from "./viewerPresentation"; @@ -135,16 +138,16 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho for (const system of systems) { 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 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 systemIcon = createTacticalIcon(context.documentRef, system.starColor, 96); const shellReticle = createShellReticle(context.documentRef, "#ff3b30", 400); const summaryVisual = createSystemSummaryVisual( 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)); root.add(starCluster, systemIcon, shellReticle, summaryVisual.sprite, detailGroup); @@ -157,7 +160,7 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho shellReticleBaseScale: 400, detailGroup, summary: summaryVisual, - galaxyPosition: toThreeVector(system.galaxyPosition), + galaxyPosition: toDisplayGalaxyVector(system.galaxyPosition), }); context.systemSummaryVisuals.set(system.id, summaryVisual); 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()) { 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( new THREE.SphereGeometry(renderedPlanetRadius, 18, 18), new THREE.MeshStandardMaterial({ @@ -176,7 +179,7 @@ export function rebuildSystems(context: SceneSyncContext, systems: SystemSnapsho 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)); planetIcon.setPosition(rawObject(planetMesh).position.clone()); const ring = planet.hasRing ? createPlanetRing(planet) : undefined; diff --git a/apps/viewer/src/viewerSelection.ts b/apps/viewer/src/viewerSelection.ts index 61a744e..ed4d15b 100644 --- a/apps/viewer/src/viewerSelection.ts +++ b/apps/viewer/src/viewerSelection.ts @@ -6,6 +6,7 @@ import type { SelectionGroup, WorldState, } from "./viewerTypes"; +import { formatGalaxyDistance } from "./viewerMath"; export function describeSelectable(world: WorldState | undefined, item: Selectable): string { if (!world) { @@ -38,6 +39,83 @@ export function describeSelectable(world: WorldState | undefined, item: Selectab 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 { if (item.kind === "ship") { return "ships"; @@ -209,7 +287,7 @@ export function renderSystemDetails(

Planets ${system.planets.length}
Moons ${moonCount}
Ships ${shipCount}
Stations ${stationCount}

Spatial nodes ${spatialNodeCount}
Resource nodes ${nodeCount}
Bubbles ${bubbleCount}

Claims ${claimCount}
Construction sites ${constructionCount}

-

Height ${system.galaxyPosition.y.toFixed(0)}

+

Height ${formatGalaxyDistance(system.galaxyPosition.y)}

${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("
")}

${followText} `; diff --git a/apps/viewer/src/viewerWorldPresentation.ts b/apps/viewer/src/viewerWorldPresentation.ts index 0659a1a..1133949 100644 --- a/apps/viewer/src/viewerWorldPresentation.ts +++ b/apps/viewer/src/viewerWorldPresentation.ts @@ -5,6 +5,7 @@ import { computePlanetLocalPosition, currentWorldTimeSeconds, resolveOrbitalAnchorPosition, + toDisplayGalaxyVector, toThreeVector, } from "./viewerMath"; import { describeActiveSpace } from "./viewerSelection"; @@ -20,6 +21,7 @@ import type { LocalBubbleSnapshot, ResourceNodeDelta, ResourceNodeSnapshot, + ShipSnapshot, } from "./contracts"; import type { BubbleVisual, @@ -52,6 +54,7 @@ export interface WorldOrbitalContext { export interface WorldPresentationContext extends WorldOrbitalContext { activeSystemId?: string; + zoomLevel: ZoomLevel; orbitYaw: number; camera: THREE.PerspectiveCamera; systemFocusLocal: THREE.Vector3; @@ -79,12 +82,19 @@ export interface GameStatusParams { export function updateWorldPresentation(context: WorldPresentationContext) { const now = performance.now(); 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); - 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()); - const shipVisible = visual.systemId === context.activeSystemId; + const shipVisible = isShipVisible(renderMode, context.activeSystemId, ship); visual.mesh.setVisible(shipVisible); visual.icon.setVisible(shipVisible && rawObject(visual.icon).visible); const desiredHeading = resolveShipHeading(visual, worldPosition, context.orbitYaw); @@ -148,6 +158,49 @@ export function updateWorldPresentation(context: WorldPresentationContext) { 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, + 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) { if (!world) { return; diff --git a/shared/data/recipes.json b/shared/data/recipes.json index 5d7d518..bd5a2d6 100644 --- a/shared/data/recipes.json +++ b/shared/data/recipes.json @@ -5,12 +5,21 @@ "facilityCategory": "station", "duration": 8, "priority": 100, - "requiredModules": ["refinery-stack", "power-core"], + "requiredModules": [ + "refinery-stack", + "power-core" + ], "inputs": [ - { "itemId": "ore", "amount": 60 } + { + "itemId": "ore", + "amount": 60 + } ], "outputs": [ - { "itemId": "refined-metals", "amount": 60 } + { + "itemId": "refined-metals", + "amount": 60 + } ] }, { @@ -19,26 +28,20 @@ "facilityCategory": "station", "duration": 7, "priority": 8, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 16 } + { + "itemId": "refined-metals", + "amount": 16 + } ], "outputs": [ - { "itemId": "ore", "amount": 24 } - ] - }, - { - "id": "gas-synthesis", - "label": "Gas Synthesis", - "facilityCategory": "station", - "duration": 6, - "priority": 12, - "requiredModules": ["fabricator-array"], - "inputs": [ - { "itemId": "refined-metals", "amount": 10 } - ], - "outputs": [ - { "itemId": "gas", "amount": 20 } + { + "itemId": "ore", + "amount": 24 + } ] }, { @@ -47,12 +50,21 @@ "facilityCategory": "station", "duration": 6, "priority": 96, - "requiredModules": ["fuel-processor", "power-core"], + "requiredModules": [ + "fuel-processor", + "power-core" + ], "inputs": [ - { "itemId": "gas", "amount": 20 } + { + "itemId": "gas", + "amount": 20 + } ], "outputs": [ - { "itemId": "fuel", "amount": 20 } + { + "itemId": "fuel", + "amount": 20 + } ] }, { @@ -61,9 +73,15 @@ "facilityCategory": "station", "duration": 12, "priority": 72, - "requiredModules": ["solar-array", "liquid-tank"], + "requiredModules": [ + "solar-array", + "liquid-tank" + ], "outputs": [ - { "itemId": "energy-cell", "amount": 6 } + { + "itemId": "energy-cell", + "amount": 6 + } ] }, { @@ -72,12 +90,20 @@ "facilityCategory": "farm", "duration": 6, "priority": 14, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "gas", "amount": 8 } + { + "itemId": "gas", + "amount": 8 + } ], "outputs": [ - { "itemId": "water", "amount": 18 } + { + "itemId": "water", + "amount": 18 + } ] }, { @@ -86,13 +112,24 @@ "facilityCategory": "station", "duration": 7, "priority": 18, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 12 }, - { "itemId": "ship-equipment", "amount": 6 } + { + "itemId": "refined-metals", + "amount": 12 + }, + { + "itemId": "ship-equipment", + "amount": 6 + } ], "outputs": [ - { "itemId": "drone-parts", "amount": 16 } + { + "itemId": "drone-parts", + "amount": 16 + } ] }, { @@ -101,12 +138,20 @@ "facilityCategory": "station", "duration": 10, "priority": 40, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 70 } + { + "itemId": "refined-metals", + "amount": 70 + } ], "outputs": [ - { "itemId": "hull-sections", "amount": 35 } + { + "itemId": "hull-sections", + "amount": 35 + } ] }, { @@ -115,12 +160,20 @@ "facilityCategory": "station", "duration": 6, "priority": 34, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 24 } + { + "itemId": "refined-metals", + "amount": 24 + } ], "outputs": [ - { "itemId": "ammo-crates", "amount": 30 } + { + "itemId": "ammo-crates", + "amount": 30 + } ] }, { @@ -129,12 +182,20 @@ "facilityCategory": "station", "duration": 9, "priority": 32, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 36 } + { + "itemId": "refined-metals", + "amount": 36 + } ], "outputs": [ - { "itemId": "naval-guns", "amount": 12 } + { + "itemId": "naval-guns", + "amount": 12 + } ] }, { @@ -143,13 +204,24 @@ "facilityCategory": "station", "duration": 11, "priority": 30, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 28 }, - { "itemId": "water", "amount": 8 } + { + "itemId": "refined-metals", + "amount": 28 + }, + { + "itemId": "water", + "amount": 8 + } ], "outputs": [ - { "itemId": "ship-equipment", "amount": 18 } + { + "itemId": "ship-equipment", + "amount": 18 + } ] }, { @@ -158,14 +230,28 @@ "facilityCategory": "station", "duration": 14, "priority": 50, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "hull-sections", "amount": 24 }, - { "itemId": "naval-guns", "amount": 6 }, - { "itemId": "ship-equipment", "amount": 10 } + { + "itemId": "hull-sections", + "amount": 24 + }, + { + "itemId": "naval-guns", + "amount": 6 + }, + { + "itemId": "ship-equipment", + "amount": 10 + } ], "outputs": [ - { "itemId": "ship-parts", "amount": 20 } + { + "itemId": "ship-parts", + "amount": 20 + } ] }, { @@ -174,13 +260,25 @@ "facilityCategory": "station", "duration": 9, "priority": 52, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 20 }, - { "itemId": "ship-equipment", "amount": 10 } + { + "itemId": "refined-metals", + "amount": 20 + }, + { + "itemId": "ship-equipment", + "amount": 10 + } ], "outputs": [ - { "itemId": "command-bridge-module", "amount": 1 } + { + "itemId": "command-bridge-module", + "amount": 1 + } ] }, { @@ -189,13 +287,25 @@ "facilityCategory": "station", "duration": 10, "priority": 54, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 30 }, - { "itemId": "ship-equipment", "amount": 8 } + { + "itemId": "refined-metals", + "amount": 30 + }, + { + "itemId": "ship-equipment", + "amount": 8 + } ], "outputs": [ - { "itemId": "reactor-core-module", "amount": 1 } + { + "itemId": "reactor-core-module", + "amount": 1 + } ] }, { @@ -204,13 +314,25 @@ "facilityCategory": "station", "duration": 9, "priority": 52, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 18 }, - { "itemId": "energy-cell", "amount": 6 } + { + "itemId": "refined-metals", + "amount": 18 + }, + { + "itemId": "energy-cell", + "amount": 6 + } ], "outputs": [ - { "itemId": "capacitor-bank-module", "amount": 1 } + { + "itemId": "capacitor-bank-module", + "amount": 1 + } ] }, { @@ -219,13 +341,25 @@ "facilityCategory": "station", "duration": 10, "priority": 53, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 22 }, - { "itemId": "ship-equipment", "amount": 8 } + { + "itemId": "refined-metals", + "amount": 22 + }, + { + "itemId": "ship-equipment", + "amount": 8 + } ], "outputs": [ - { "itemId": "ion-drive-module", "amount": 1 } + { + "itemId": "ion-drive-module", + "amount": 1 + } ] }, { @@ -234,14 +368,29 @@ "facilityCategory": "station", "duration": 12, "priority": 56, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 34 }, - { "itemId": "ship-equipment", "amount": 14 }, - { "itemId": "fuel", "amount": 12 } + { + "itemId": "refined-metals", + "amount": 34 + }, + { + "itemId": "ship-equipment", + "amount": 14 + }, + { + "itemId": "fuel", + "amount": 12 + } ], "outputs": [ - { "itemId": "ftl-core-module", "amount": 1 } + { + "itemId": "ftl-core-module", + "amount": 1 + } ] }, { @@ -250,13 +399,25 @@ "facilityCategory": "station", "duration": 8, "priority": 58, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "naval-guns", "amount": 8 }, - { "itemId": "refined-metals", "amount": 12 } + { + "itemId": "naval-guns", + "amount": 8 + }, + { + "itemId": "refined-metals", + "amount": 12 + } ], "outputs": [ - { "itemId": "gun-turret-module", "amount": 1 } + { + "itemId": "gun-turret-module", + "amount": 1 + } ] }, { @@ -265,14 +426,29 @@ "facilityCategory": "station", "duration": 14, "priority": 40, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "hull-sections", "amount": 18 }, - { "itemId": "refined-metals", "amount": 18 }, - { "itemId": "ship-equipment", "amount": 10 } + { + "itemId": "hull-sections", + "amount": 18 + }, + { + "itemId": "refined-metals", + "amount": 18 + }, + { + "itemId": "ship-equipment", + "amount": 10 + } ], "outputs": [ - { "itemId": "carrier-bay-module", "amount": 1 } + { + "itemId": "carrier-bay-module", + "amount": 1 + } ] }, { @@ -281,14 +457,29 @@ "facilityCategory": "station", "duration": 12, "priority": 22, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "hull-sections", "amount": 14 }, - { "itemId": "ship-equipment", "amount": 8 }, - { "itemId": "water", "amount": 10 } + { + "itemId": "hull-sections", + "amount": 14 + }, + { + "itemId": "ship-equipment", + "amount": 8 + }, + { + "itemId": "water", + "amount": 10 + } ], "outputs": [ - { "itemId": "habitat-ring-module", "amount": 1 } + { + "itemId": "habitat-ring-module", + "amount": 1 + } ] }, { @@ -297,13 +488,25 @@ "facilityCategory": "station", "duration": 8, "priority": 18, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 16 }, - { "itemId": "hull-sections", "amount": 10 } + { + "itemId": "refined-metals", + "amount": 16 + }, + { + "itemId": "hull-sections", + "amount": 10 + } ], "outputs": [ - { "itemId": "bulk-bay-module", "amount": 1 } + { + "itemId": "bulk-bay-module", + "amount": 1 + } ] }, { @@ -312,13 +515,25 @@ "facilityCategory": "station", "duration": 8, "priority": 18, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 12 }, - { "itemId": "ship-equipment", "amount": 4 } + { + "itemId": "refined-metals", + "amount": 12 + }, + { + "itemId": "ship-equipment", + "amount": 4 + } ], "outputs": [ - { "itemId": "container-bay-module", "amount": 1 } + { + "itemId": "container-bay-module", + "amount": 1 + } ] }, { @@ -327,13 +542,25 @@ "facilityCategory": "station", "duration": 8, "priority": 18, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 14 }, - { "itemId": "ship-equipment", "amount": 4 } + { + "itemId": "refined-metals", + "amount": 14 + }, + { + "itemId": "ship-equipment", + "amount": 4 + } ], "outputs": [ - { "itemId": "liquid-tank-module", "amount": 1 } + { + "itemId": "liquid-tank-module", + "amount": 1 + } ] }, { @@ -342,13 +569,25 @@ "facilityCategory": "station", "duration": 8, "priority": 18, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 14 }, - { "itemId": "ship-equipment", "amount": 4 } + { + "itemId": "refined-metals", + "amount": 14 + }, + { + "itemId": "ship-equipment", + "amount": 4 + } ], "outputs": [ - { "itemId": "gas-tank-module", "amount": 1 } + { + "itemId": "gas-tank-module", + "amount": 1 + } ] }, { @@ -357,13 +596,25 @@ "facilityCategory": "station", "duration": 9, "priority": 24, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 18 }, - { "itemId": "ship-equipment", "amount": 6 } + { + "itemId": "refined-metals", + "amount": 18 + }, + { + "itemId": "ship-equipment", + "amount": 6 + } ], "outputs": [ - { "itemId": "mining-turret-module", "amount": 1 } + { + "itemId": "mining-turret-module", + "amount": 1 + } ] }, { @@ -372,14 +623,29 @@ "facilityCategory": "station", "duration": 9, "priority": 24, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 18 }, - { "itemId": "ship-equipment", "amount": 6 }, - { "itemId": "gas", "amount": 8 } + { + "itemId": "refined-metals", + "amount": 18 + }, + { + "itemId": "ship-equipment", + "amount": 6 + }, + { + "itemId": "gas", + "amount": 8 + } ], "outputs": [ - { "itemId": "gas-extractor-module", "amount": 1 } + { + "itemId": "gas-extractor-module", + "amount": 1 + } ] }, { @@ -388,13 +654,25 @@ "facilityCategory": "station", "duration": 11, "priority": 20, - "requiredModules": ["component-factory", "container-bay"], + "requiredModules": [ + "component-factory", + "container-bay" + ], "inputs": [ - { "itemId": "refined-metals", "amount": 24 }, - { "itemId": "ship-equipment", "amount": 10 } + { + "itemId": "refined-metals", + "amount": 24 + }, + { + "itemId": "ship-equipment", + "amount": 10 + } ], "outputs": [ - { "itemId": "fabricator-array-module", "amount": 1 } + { + "itemId": "fabricator-array-module", + "amount": 1 + } ] }, { @@ -403,17 +681,46 @@ "facilityCategory": "station", "duration": 24, "priority": 90, - "requiredModules": ["ship-factory", "dock-bay-small", "container-bay", "power-core"], + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], "shipOutputId": "frigate", "inputs": [ - { "itemId": "hull-sections", "amount": 26 }, - { "itemId": "fuel", "amount": 40 }, - { "itemId": "command-bridge-module", "amount": 1 }, - { "itemId": "reactor-core-module", "amount": 1 }, - { "itemId": "capacitor-bank-module", "amount": 1 }, - { "itemId": "ion-drive-module", "amount": 1 }, - { "itemId": "ftl-core-module", "amount": 1 }, - { "itemId": "gun-turret-module", "amount": 1 } + { + "itemId": "hull-sections", + "amount": 26 + }, + { + "itemId": "fuel", + "amount": 40 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "gun-turret-module", + "amount": 1 + } ], "outputs": [] }, @@ -423,17 +730,46 @@ "facilityCategory": "station", "duration": 34, "priority": 70, - "requiredModules": ["ship-factory", "dock-bay-small", "container-bay", "power-core"], + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], "shipOutputId": "destroyer", "inputs": [ - { "itemId": "hull-sections", "amount": 44 }, - { "itemId": "fuel", "amount": 60 }, - { "itemId": "command-bridge-module", "amount": 1 }, - { "itemId": "reactor-core-module", "amount": 1 }, - { "itemId": "capacitor-bank-module", "amount": 1 }, - { "itemId": "ion-drive-module", "amount": 1 }, - { "itemId": "ftl-core-module", "amount": 1 }, - { "itemId": "gun-turret-module", "amount": 2 } + { + "itemId": "hull-sections", + "amount": 44 + }, + { + "itemId": "fuel", + "amount": 60 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "gun-turret-module", + "amount": 2 + } ], "outputs": [] }, @@ -443,17 +779,46 @@ "facilityCategory": "station", "duration": 42, "priority": 54, - "requiredModules": ["ship-factory", "dock-bay-small", "container-bay", "power-core"], + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], "shipOutputId": "cruiser", "inputs": [ - { "itemId": "hull-sections", "amount": 60 }, - { "itemId": "fuel", "amount": 80 }, - { "itemId": "command-bridge-module", "amount": 1 }, - { "itemId": "reactor-core-module", "amount": 1 }, - { "itemId": "capacitor-bank-module", "amount": 1 }, - { "itemId": "ion-drive-module", "amount": 1 }, - { "itemId": "ftl-core-module", "amount": 1 }, - { "itemId": "gun-turret-module", "amount": 2 } + { + "itemId": "hull-sections", + "amount": 60 + }, + { + "itemId": "fuel", + "amount": 80 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "gun-turret-module", + "amount": 2 + } ], "outputs": [] }, @@ -463,19 +828,54 @@ "facilityCategory": "station", "duration": 60, "priority": 28, - "requiredModules": ["ship-factory", "dock-bay-small", "container-bay", "power-core"], + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], "shipOutputId": "carrier", "inputs": [ - { "itemId": "hull-sections", "amount": 120 }, - { "itemId": "fuel", "amount": 140 }, - { "itemId": "command-bridge-module", "amount": 1 }, - { "itemId": "reactor-core-module", "amount": 1 }, - { "itemId": "capacitor-bank-module", "amount": 1 }, - { "itemId": "ion-drive-module", "amount": 1 }, - { "itemId": "ftl-core-module", "amount": 1 }, - { "itemId": "carrier-bay-module", "amount": 2 }, - { "itemId": "gun-turret-module", "amount": 1 }, - { "itemId": "habitat-ring-module", "amount": 1 } + { + "itemId": "hull-sections", + "amount": 120 + }, + { + "itemId": "fuel", + "amount": 140 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "carrier-bay-module", + "amount": 2 + }, + { + "itemId": "gun-turret-module", + "amount": 1 + }, + { + "itemId": "habitat-ring-module", + "amount": 1 + } ], "outputs": [] }, @@ -485,17 +885,46 @@ "facilityCategory": "station", "duration": 26, "priority": 8, - "requiredModules": ["ship-factory", "dock-bay-small", "container-bay", "power-core"], + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], "shipOutputId": "hauler", "inputs": [ - { "itemId": "hull-sections", "amount": 34 }, - { "itemId": "fuel", "amount": 40 }, - { "itemId": "command-bridge-module", "amount": 1 }, - { "itemId": "reactor-core-module", "amount": 1 }, - { "itemId": "capacitor-bank-module", "amount": 1 }, - { "itemId": "ion-drive-module", "amount": 1 }, - { "itemId": "ftl-core-module", "amount": 1 }, - { "itemId": "liquid-tank-module", "amount": 1 } + { + "itemId": "hull-sections", + "amount": 34 + }, + { + "itemId": "fuel", + "amount": 40 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "liquid-tank-module", + "amount": 1 + } ], "outputs": [] }, @@ -505,18 +934,50 @@ "facilityCategory": "station", "duration": 30, "priority": 8, - "requiredModules": ["ship-factory", "dock-bay-small", "container-bay", "power-core"], + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], "shipOutputId": "constructor", "inputs": [ - { "itemId": "hull-sections", "amount": 42 }, - { "itemId": "fuel", "amount": 44 }, - { "itemId": "command-bridge-module", "amount": 1 }, - { "itemId": "reactor-core-module", "amount": 1 }, - { "itemId": "capacitor-bank-module", "amount": 1 }, - { "itemId": "ion-drive-module", "amount": 1 }, - { "itemId": "ftl-core-module", "amount": 1 }, - { "itemId": "fabricator-array-module", "amount": 1 }, - { "itemId": "container-bay-module", "amount": 1 } + { + "itemId": "hull-sections", + "amount": 42 + }, + { + "itemId": "fuel", + "amount": 44 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "fabricator-array-module", + "amount": 1 + }, + { + "itemId": "container-bay-module", + "amount": 1 + } ], "outputs": [] }, @@ -526,18 +987,50 @@ "facilityCategory": "station", "duration": 28, "priority": 8, - "requiredModules": ["ship-factory", "dock-bay-small", "container-bay", "power-core"], + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], "shipOutputId": "miner", "inputs": [ - { "itemId": "hull-sections", "amount": 34 }, - { "itemId": "fuel", "amount": 42 }, - { "itemId": "command-bridge-module", "amount": 1 }, - { "itemId": "reactor-core-module", "amount": 1 }, - { "itemId": "capacitor-bank-module", "amount": 1 }, - { "itemId": "ion-drive-module", "amount": 1 }, - { "itemId": "ftl-core-module", "amount": 1 }, - { "itemId": "mining-turret-module", "amount": 1 }, - { "itemId": "bulk-bay-module", "amount": 1 } + { + "itemId": "hull-sections", + "amount": 34 + }, + { + "itemId": "fuel", + "amount": 42 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "mining-turret-module", + "amount": 1 + }, + { + "itemId": "bulk-bay-module", + "amount": 1 + } ], "outputs": [] }, @@ -547,18 +1040,50 @@ "facilityCategory": "station", "duration": 28, "priority": 8, - "requiredModules": ["ship-factory", "dock-bay-small", "container-bay", "power-core"], + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], "shipOutputId": "gas-miner", "inputs": [ - { "itemId": "hull-sections", "amount": 34 }, - { "itemId": "fuel", "amount": 42 }, - { "itemId": "command-bridge-module", "amount": 1 }, - { "itemId": "reactor-core-module", "amount": 1 }, - { "itemId": "capacitor-bank-module", "amount": 1 }, - { "itemId": "ion-drive-module", "amount": 1 }, - { "itemId": "ftl-core-module", "amount": 1 }, - { "itemId": "gas-extractor-module", "amount": 1 }, - { "itemId": "gas-tank-module", "amount": 1 } + { + "itemId": "hull-sections", + "amount": 34 + }, + { + "itemId": "fuel", + "amount": 42 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "gas-extractor-module", + "amount": 1 + }, + { + "itemId": "gas-tank-module", + "amount": 1 + } ], "outputs": [] }, @@ -568,14 +1093,28 @@ "facilityCategory": "station", "duration": 18, "priority": 24, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "ship-parts", "amount": 26 }, - { "itemId": "ship-equipment", "amount": 16 }, - { "itemId": "drone-parts", "amount": 10 } + { + "itemId": "ship-parts", + "amount": 26 + }, + { + "itemId": "ship-equipment", + "amount": 16 + }, + { + "itemId": "drone-parts", + "amount": 10 + } ], "outputs": [ - { "itemId": "trade-hub-kit", "amount": 1 } + { + "itemId": "trade-hub-kit", + "amount": 1 + } ] }, { @@ -584,14 +1123,28 @@ "facilityCategory": "station", "duration": 20, "priority": 26, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "ship-parts", "amount": 32 }, - { "itemId": "hull-sections", "amount": 24 }, - { "itemId": "ship-equipment", "amount": 14 } + { + "itemId": "ship-parts", + "amount": 32 + }, + { + "itemId": "hull-sections", + "amount": 24 + }, + { + "itemId": "ship-equipment", + "amount": 14 + } ], "outputs": [ - { "itemId": "refinery-kit", "amount": 1 } + { + "itemId": "refinery-kit", + "amount": 1 + } ] }, { @@ -600,14 +1153,28 @@ "facilityCategory": "station", "duration": 18, "priority": 22, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "ship-parts", "amount": 22 }, - { "itemId": "ship-equipment", "amount": 18 }, - { "itemId": "water", "amount": 22 } + { + "itemId": "ship-parts", + "amount": 22 + }, + { + "itemId": "ship-equipment", + "amount": 18 + }, + { + "itemId": "water", + "amount": 22 + } ], "outputs": [ - { "itemId": "farm-ring-kit", "amount": 1 } + { + "itemId": "farm-ring-kit", + "amount": 1 + } ] }, { @@ -616,14 +1183,28 @@ "facilityCategory": "station", "duration": 22, "priority": 28, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "ship-parts", "amount": 34 }, - { "itemId": "hull-sections", "amount": 16 }, - { "itemId": "ship-equipment", "amount": 18 } + { + "itemId": "ship-parts", + "amount": 34 + }, + { + "itemId": "hull-sections", + "amount": 16 + }, + { + "itemId": "ship-equipment", + "amount": 18 + } ], "outputs": [ - { "itemId": "manufactory-kit", "amount": 1 } + { + "itemId": "manufactory-kit", + "amount": 1 + } ] }, { @@ -632,14 +1213,28 @@ "facilityCategory": "station", "duration": 26, "priority": 30, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "ship-parts", "amount": 42 }, - { "itemId": "hull-sections", "amount": 30 }, - { "itemId": "naval-guns", "amount": 10 } + { + "itemId": "ship-parts", + "amount": 42 + }, + { + "itemId": "hull-sections", + "amount": 30 + }, + { + "itemId": "naval-guns", + "amount": 10 + } ], "outputs": [ - { "itemId": "shipyard-kit", "amount": 1 } + { + "itemId": "shipyard-kit", + "amount": 1 + } ] }, { @@ -648,14 +1243,28 @@ "facilityCategory": "station", "duration": 16, "priority": 20, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "ship-parts", "amount": 18 }, - { "itemId": "naval-guns", "amount": 12 }, - { "itemId": "ammo-crates", "amount": 18 } + { + "itemId": "ship-parts", + "amount": 18 + }, + { + "itemId": "naval-guns", + "amount": 12 + }, + { + "itemId": "ammo-crates", + "amount": 18 + } ], "outputs": [ - { "itemId": "defense-grid-kit", "amount": 1 } + { + "itemId": "defense-grid-kit", + "amount": 1 + } ] }, { @@ -664,15 +1273,32 @@ "facilityCategory": "station", "duration": 34, "priority": 36, - "requiredModules": ["fabricator-array"], + "requiredModules": [ + "fabricator-array" + ], "inputs": [ - { "itemId": "ship-parts", "amount": 60 }, - { "itemId": "hull-sections", "amount": 44 }, - { "itemId": "ship-equipment", "amount": 26 }, - { "itemId": "naval-guns", "amount": 8 } + { + "itemId": "ship-parts", + "amount": 60 + }, + { + "itemId": "hull-sections", + "amount": 44 + }, + { + "itemId": "ship-equipment", + "amount": 26 + }, + { + "itemId": "naval-guns", + "amount": 8 + } ], "outputs": [ - { "itemId": "stargate-kit", "amount": 1 } + { + "itemId": "stargate-kit", + "amount": 1 + } ] } ] diff --git a/shared/data/scenario.json b/shared/data/scenario.json index a0ce0c5..9f8997f 100644 --- a/shared/data/scenario.json +++ b/shared/data/scenario.json @@ -1,11 +1,53 @@ { "initialStations": [ - { "constructibleId": "station-core", "systemId": "helios", "planetIndex": 2, "lagrangeSide": -1 } + { + "constructibleId": "station-core", + "systemId": "helios", + "planetIndex": 2, + "lagrangeSide": -1 + } ], "shipFormations": [ - { "shipId": "constructor", "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": "constructor", + "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": [], "miningDefaults": { diff --git a/shared/data/ships.json b/shared/data/ships.json index 68bf0cd..4451927 100644 --- a/shared/data/ships.json +++ b/shared/data/ships.json @@ -4,8 +4,9 @@ "label": "Vanguard Frigate", "role": "military", "shipClass": "frigate", - "speed": 50, - "ftlSpeed": 3200, + "speed": 120000, + "warpSpeed": 0.22, + "ftlSpeed": 0.75, "spoolTime": 2.2, "cargoCapacity": 0, "color": "#7ed4ff", @@ -19,8 +20,9 @@ "label": "Bulwark Destroyer", "role": "military", "shipClass": "destroyer", - "speed": 34, - "ftlSpeed": 2900, + "speed": 95000, + "warpSpeed": 0.18, + "ftlSpeed": 0.68, "spoolTime": 2.8, "cargoCapacity": 0, "color": "#ff8f70", @@ -34,8 +36,9 @@ "label": "Aegis Cruiser", "role": "military", "shipClass": "cruiser", - "speed": 28, - "ftlSpeed": 2750, + "speed": 85000, + "warpSpeed": 0.16, + "ftlSpeed": 0.62, "spoolTime": 3.1, "cargoCapacity": 0, "color": "#9ec1ff", @@ -49,8 +52,9 @@ "label": "Citadel Carrier", "role": "military", "shipClass": "capital", - "speed": 18, - "ftlSpeed": 2500, + "speed": 60000, + "warpSpeed": 0.12, + "ftlSpeed": 0.5, "spoolTime": 4.1, "cargoCapacity": 0, "color": "#c6f4ff", @@ -66,8 +70,9 @@ "label": "Atlas Hauler", "role": "transport", "shipClass": "industrial", - "speed": 22, - "ftlSpeed": 2600, + "speed": 70000, + "warpSpeed": 0.14, + "ftlSpeed": 0.55, "spoolTime": 3.3, "cargoCapacity": 180, "cargoKind": "bulk-liquid", @@ -83,8 +88,9 @@ "label": "Pioneer Constructor", "role": "construction", "shipClass": "industrial", - "speed": 20, - "ftlSpeed": 2200, + "speed": 65000, + "warpSpeed": 0.13, + "ftlSpeed": 0.48, "spoolTime": 3.5, "cargoCapacity": 160, "cargoKind": "manufactured", @@ -100,8 +106,9 @@ "label": "Prospector Miner", "role": "mining", "shipClass": "industrial", - "speed": 26, - "ftlSpeed": 2400, + "speed": 75000, + "warpSpeed": 0.15, + "ftlSpeed": 0.5, "spoolTime": 3.1, "cargoCapacity": 120, "cargoKind": "bulk-solid", @@ -117,8 +124,9 @@ "label": "Nimbus Gas Harvester", "role": "mining", "shipClass": "industrial", - "speed": 24, - "ftlSpeed": 2350, + "speed": 72000, + "warpSpeed": 0.145, + "ftlSpeed": 0.49, "spoolTime": 3.2, "cargoCapacity": 120, "cargoKind": "bulk-gas", diff --git a/shared/data/systems.json b/shared/data/systems.json index 74ce5f4..73dbfd7 100644 --- a/shared/data/systems.json +++ b/shared/data/systems.json @@ -5,45 +5,45 @@ "position": [0, 0, 0], "starColor": "#ffd27a", "starGlow": "#ffb14a", - "starSize": 56, + "starSize": 720000, "gravityWellRadius": 210, "asteroidField": { "decorationCount": 180, - "radiusOffset": 330, - "radiusVariance": 90, - "heightVariance": 18 + "radiusOffset": 330000, + "radiusVariance": 90000, + "heightVariance": 18000 }, "resourceNodes": [], "planets": [ - { "label": "Icarus", "orbitRadius": 180, "orbitSpeed": 0.18, "size": 20, "color": "#d4a373", "tilt": 0.2 }, - { "label": "Viridia", "orbitRadius": 300, "orbitSpeed": 0.11, "size": 30, "color": "#58a36c", "tilt": -0.4 }, - { "label": "Aster", "orbitRadius": 460, "orbitSpeed": 0.08, "size": 38, "color": "#6ea7d4", "tilt": 0.3, "hasRing": true }, - { "label": "Noctis", "orbitRadius": 670, "orbitSpeed": 0.05, "size": 50, "color": "#6958a8", "tilt": -0.15 } + { "label": "Icarus", "orbitRadius": 0.36, "orbitSpeed": 0.5093, "size": 4200, "color": "#d4a373", "tilt": 0.2 }, + { "label": "Viridia", "orbitRadius": 0.60, "orbitSpeed": 0.2366, "size": 6200, "color": "#58a36c", "tilt": -0.4 }, + { "label": "Aster", "orbitRadius": 0.92, "orbitSpeed": 0.1246, "size": 7800, "color": "#6ea7d4", "tilt": 0.3, "hasRing": true }, + { "label": "Noctis", "orbitRadius": 1.34, "orbitSpeed": 0.0710, "size": 11200, "color": "#6958a8", "tilt": -0.15 } ] }, { "id": "perseus", "label": "Perseus Gate", - "position": [4400, 0, 620], + "position": [4.4, 0, 0.62], "starColor": "#9dc6ff", "starGlow": "#66a0ff", - "starSize": 48, + "starSize": 930000, "gravityWellRadius": 230, "asteroidField": { "decorationCount": 180, - "radiusOffset": 330, - "radiusVariance": 90, - "heightVariance": 18 + "radiusOffset": 330000, + "radiusVariance": 90000, + "heightVariance": 18000 }, "resourceNodes": [ - { "angle": 0.45, "radiusOffset": 360, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 }, - { "angle": 2.544395102, "radiusOffset": 360, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 }, - { "angle": 4.638790205, "radiusOffset": 360, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 } + { "angle": 0.45, "radiusOffset": 180000, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 }, + { "angle": 2.544395102, "radiusOffset": 180000, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 }, + { "angle": 4.638790205, "radiusOffset": 180000, "oreAmount": 3000, "itemId": "ore", "shardCount": 7 } ], "planets": [ - { "label": "Talos", "orbitRadius": 200, "orbitSpeed": 0.15, "size": 24, "color": "#c48f6a", "tilt": 0.18 }, - { "label": "Cygnus", "orbitRadius": 360, "orbitSpeed": 0.1, "size": 34, "color": "#4f84c4", "tilt": -0.22, "hasRing": true }, - { "label": "Rhea", "orbitRadius": 540, "orbitSpeed": 0.07, "size": 44, "color": "#8f8fb0", "tilt": 0.08 } + { "label": "Talos", "orbitRadius": 0.40, "orbitSpeed": 0.4348, "size": 5000, "color": "#c48f6a", "tilt": 0.18 }, + { "label": "Cygnus", "orbitRadius": 0.72, "orbitSpeed": 0.1800, "size": 6900, "color": "#4f84c4", "tilt": -0.22, "hasRing": true }, + { "label": "Rhea", "orbitRadius": 1.08, "orbitSpeed": 0.0981, "size": 9600, "color": "#8f8fb0", "tilt": 0.08 } ] } ]