diff --git a/Assets/Scripts/ServerShared/WellTree.cs b/Assets/Scripts/ServerShared/WellTree.cs index c0dd3d825..8f9cd50da 100644 --- a/Assets/Scripts/ServerShared/WellTree.cs +++ b/Assets/Scripts/ServerShared/WellTree.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using GameCult.Caching; using CultMath; using static CultMath.math; +using float2 = CultMath.float2; /// An orbit as the well tree reads it: where it hangs on the tree and the mass of the well-bearing body on it (0 for none). public readonly struct WellNode diff --git a/tests/Aetheria.Shared.Tests/PotentialOwnerTests.cs b/tests/Aetheria.Shared.Tests/PotentialOwnerTests.cs index 3cd03747e..c1b492690 100644 --- a/tests/Aetheria.Shared.Tests/PotentialOwnerTests.cs +++ b/tests/Aetheria.Shared.Tests/PotentialOwnerTests.cs @@ -73,8 +73,8 @@ public sealed class PotentialOwnerTests : IDisposable private static void Near(float expected, float actual, float relative) => Assert.InRange(actual, expected - Math.Abs(expected) * relative - 1e-5f, expected + Math.Abs(expected) * relative + 1e-5f); - // The law written out: potential c r^2 / 2R^2 inside the core, c (ln(r / R) + 1/2) beyond. - private static float Phi(float c, float core, float r) => r < core ? c * r * r / (2 * core * core) : c * (log(r / core) + .5f); + // The law of a lone body written out (its global term): the Plummer potential c ln(r^2 + eps^2) / 2, eps the Roche limit. + private static float Phi(float c, float eps, float r) => c * .5f * log(r * r + eps * eps); // A zone whose bodies have no authored gravity curves (most test zones) has no pull and no dip, and no NaN either. [Fact] @@ -103,12 +103,13 @@ public sealed class PotentialOwnerTests : IDisposable var zone = ZoneOf(planets, (mass, float2.zero)); var core = planets.RocheLimit.Evaluate(mass); foreach (var r in new[] { 3f * core, 10f * core, PackRadius / 2f }) - Near(planets.WellStrength.Evaluate(mass), length(zone.GetForce(new float2(r, 0))) * r, 1e-3f); + Near(planets.WellStrength.Evaluate(mass) * r * r / (r * r + core * core), length(zone.GetForce(new float2(r, 0))) * r, 1e-3f); } } + // The steepest pull of the Plummer well is at its softening, which is the Roche curve: it rises up to it and falls beyond. [Fact] - public void TheCoreRadiusIsTheRocheCurve() + public void TheSteepestPullIsAtTheRocheCurve() { var planets = Curves(); foreach (var mass in new[] { 29.2f, 14152.4f }) @@ -116,10 +117,8 @@ public sealed class PotentialOwnerTests : IDisposable var zone = ZoneOf(planets, (mass, float2.zero)); var core = planets.RocheLimit.Evaluate(mass); float Pull(float r) => length(zone.GetForce(new float2(0, r))); - var slope = Pull(.25f * core) / (.25f * core); - Near(slope, Pull(.5f * core) / (.5f * core), 1e-3f); - Near(slope, Pull(.9f * core) / (.9f * core), 1e-3f); - Assert.True(Pull(core) > Pull(.98f * core), "the pull still rises up to the core radius"); + Near(planets.WellStrength.Evaluate(mass) / (2f * core), Pull(core), 1e-3f); + Assert.True(Pull(core) > Pull(.98f * core), "the pull still rises up to the Roche limit"); Assert.True(Pull(core) > Pull(1.02f * core), "and falls beyond it"); } } @@ -146,10 +145,10 @@ public sealed class PotentialOwnerTests : IDisposable ZoneOf(Constants(50f), (100f, float2.zero)); } - // core-at-roche-and-exaggeration-up: growing the core to the Roche limit (3 m^.25 to 7.32 m^.25, 2.44x) and dividing the - // global K by that factor leaves the steepest drawn slope (|grad phi| / K, at the core) where it was, and draws every - // slope outside the old core 2.44x steeper. The "before" side is the previous authoring, stated here from the ruling: - // core 3 m^.25 and K 10. The "after" side is the shipped Settings.asset read through the production zone. + // core-at-roche-and-exaggeration-up: the Plummer well's steepest pull is 0.5 c / eps at the Roche limit (eps = 7.32 m^.25), + // half the harmonic core's c / R, so K halves with the law and the steepest drawn slope stays where the previous authoring + // had it. The "before" side is stated from the ruling: core 3 m^.25 and K 10. The "after" side is the shipped + // Settings.asset read through the production zone. Outside the old core the funnel draws steeper than it did. [Fact] public void ThePeakDrawnSlopeStaysAndTheFunnelDrawsSteeper() { @@ -165,10 +164,8 @@ public sealed class PotentialOwnerTests : IDisposable var peakBefore = c / oldCore / oldExaggeration; Assert.InRange(Drawn(planets.RocheLimit.Evaluate(mass)), peakBefore * .98f, peakBefore * 1.02f); - var rocheOverOldCore = planets.RocheLimit.Evaluate(mass) / oldCore; var farBefore = c / (3f * oldCore) / oldExaggeration; - Near(rocheOverOldCore, Drawn(3f * oldCore) / farBefore, 1e-2f); - Assert.InRange(Drawn(3f * oldCore) / farBefore, 2.44f * .99f, 2.44f * 1.01f); + Assert.InRange(Drawn(3f * oldCore) / farBefore, 2.9f, 2.98f); } } @@ -247,8 +244,9 @@ public sealed class PotentialOwnerTests : IDisposable } } + // A lone body is a global term: it pulls as the Plummer well at every range, with no rim and no gate. [Fact] - public void TheTailIsAPointMassWithNoRimAndNoGate() + public void TheGlobalTermIsPlummerAtEveryRange() { var planets = Curves(); var zone = ZoneOf(planets, (273.9f, float2.zero)); @@ -259,7 +257,7 @@ public sealed class PotentialOwnerTests : IDisposable { var at = new float2(r * cos(angle), r * sin(angle)); var force = zone.GetForce(at); - Near(c, length(force) * r, 1e-3f); + Near(c * r * r / (r * r + core * core), length(force) * r, 1e-3f); Assert.True(dot(normalize(force), normalize(-at)) > .9999f, "the pull points at the body"); } } @@ -297,7 +295,7 @@ public sealed class PotentialOwnerTests : IDisposable var c = planets.WellStrength.Evaluate(273.9f); var core = planets.RocheLimit.Evaluate(273.9f); Assert.Equal(0f, lone.GetHeight(new float2(PackRadius, 0)), 4); - Near(-c * (log(PackRadius / core) + .5f) / k, lone.GetHeight(float2.zero), 1e-4f); + Near(-c * .5f * log((PackRadius * PackRadius + core * core) / (core * core)) / k, lone.GetHeight(float2.zero), 1e-4f); var two = ZoneOf(planets, (29.2f, new float2(-300f, 0f)), (14152.4f, new float2(300f, 0f))); var c0 = planets.WellStrength.Evaluate(29.2f); @@ -305,7 +303,7 @@ public sealed class PotentialOwnerTests : IDisposable var c1 = planets.WellStrength.Evaluate(14152.4f); var core1 = planets.RocheLimit.Evaluate(14152.4f); const float step = .5f; - // Inside the two cores as well, out to their outer bands (0.5 R to R), where the potential is the quadratic bowl. + // Inside the two softenings as well, where the Plummer potential is smooth through the centre. var inCore = new[] { float2(-300f + .5f * core0, 0f), float2(-300f, .65f * core0), float2(-300f - .86f * core0, 0f), float2(-300f + .6f * core0, .6f * core0), @@ -326,44 +324,4 @@ public sealed class PotentialOwnerTests : IDisposable } } } - - // Inside the core the potential is the bowl c r^2 / 2R^2 all the way out to R: the drop from a lone body's centre to a point - // at r is exactly that, from the middle of the core to a hair inside its edge (the log form agrees with the bowl to second - // order at R, so only a tight tolerance sees a join that sits just inside it). - [Fact] - public void TheCoreIsTheBowlUpToItsEdge() - { - foreach (var k in new[] { 1f, 10f }) - { - var planets = Curves(k); - foreach (var mass in new[] { 29.2f, 14152.4f }) - { - var zone = ZoneOf(planets, (mass, float2.zero)); - var c = planets.WellStrength.Evaluate(mass); - var core = planets.RocheLimit.Evaluate(mass); - foreach (var fraction in new[] { .5f, .75f, .9f, .95f, .98f, .99f, .996f }) - foreach (var angle in new[] { 0f, 2.3f }) - { - var r = fraction * core; - var drop = (zone.GetHeight(new float2(r * cos(angle), r * sin(angle))) - zone.GetHeight(float2.zero)) * k; - Near(c * fraction * fraction / 2f, drop, 2e-5f); - } - } - } - } - - [Fact] - public void TheCoreMeetsTheTailAtItsRadius() - { - var planets = Curves(); - foreach (var mass in new[] { 29.2f, 14152.4f }) - { - var zone = ZoneOf(planets, (mass, float2.zero)); - var core = planets.RocheLimit.Evaluate(mass); - var inside = new float2(core * (1f - 1e-3f), 0); - var outside = new float2(core * (1f + 1e-3f), 0); - Assert.InRange(abs(zone.GetHeight(inside) - zone.GetHeight(outside)), 0f, abs(zone.GetHeight(outside)) * 1e-3f); - Assert.InRange(abs(length(zone.GetForce(inside)) - length(zone.GetForce(outside))), 0f, length(zone.GetForce(outside)) * 1e-3f); - } - } }