Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
using System ;
using System.IO ;
using System.Linq ;
using CultMath ;
using GameCult.Caching ;
using Xunit ;
using static CultMath . math ;
using float2 = CultMath . float2 ;
using float3 = CultMath . float3 ;
// aetheria-gravity target r4: one potential per zone owns gravity (potential-owns-gravity), built from the authored
// curves of mass (well strength, core radius, Roche limit) and one global exaggeration. Each test builds the zone
// through the production constructor from PlanetSettings and body records and reads the owner's GetForce and
// GetHeight; the closed forms written here state the law, they are not Zone's code.
public sealed class PotentialOwnerTests : IDisposable
{
private const float PackRadius = 1000f ;
private readonly string _root = Path . Combine ( Path . GetTempPath ( ) , "aetheria-potential-" + Guid . NewGuid ( ) . ToString ( "N" ) ) ;
private readonly CultCache _cache ;
private readonly ItemManager _items ;
public PotentialOwnerTests ( )
{
Directory . CreateDirectory ( _root ) ;
_cache = AetheriaStores . Open ( Path . Combine ( _root , "pot.cc" ) , Path . Combine ( _root , "pot-run.cc" ) , catalogWritable : true ) ;
_cache . Upsert ( new VerseGrammar { Revision = 1 } ) ;
_cache . FlushAsync ( ) . Wait ( ) ;
var settings = new GameplaySettings
{
DefaultEntitySettings = new EntitySettings ( ) ,
2026-10-17 18:55:16 +02:00
Tiers = new [ ] { new RarityTier { Name = "Common" , Quality = . 5f , Color = new float3 ( 1 , 1 , 1 ) } } ,
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
QualityPriceModifier = new ExponentialLerp ( )
} ;
2026-10-21 19:01:41 +02:00
_items = new ItemManager ( 1 u , _cache , new ProvenanceLedger ( ) , settings , _ = > { } ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
}
public void Dispose ( )
{
_cache . Dispose ( ) ;
Directory . Delete ( _root , true ) ;
}
2026-10-15 16:46:53 +02:00
// The authored curves of the shipped Settings.asset: strength c = 45.08 m^.2, Roche limit and core R = 7.32 m^.25.
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
private static PlanetSettings Curves ( float exaggeration = 1f ) = > new PlanetSettings
{
WellStrength = new ExponentialCurve { Exponent = . 2f , Multiplier = 45.08f } ,
RocheLimit = new ExponentialCurve { Exponent = . 25f , Multiplier = 7.32f } ,
BodyRadius = new ExponentialCurve { Exponent = . 25f , Multiplier = 2f } ,
2026-10-15 18:11:27 +02:00
LightRadius = new ExponentialCurve ( ) ,
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
HeightExaggeration = exaggeration
} ;
private int _bodies ;
2026-10-15 06:36:02 +02:00
private Zone ZoneOf ( PlanetSettings planets , params ( float mass , float2 at ) [ ] bodies ) = > ZoneOf ( planets , null , bodies ) ;
// `author` edits each body's record before it is stored: the per-body knobs a body record carries.
private Zone ZoneOf ( PlanetSettings planets , Action < PlanetData > author , params ( float mass , float2 at ) [ ] bodies )
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
{
var pack = new ZonePack { Radius = PackRadius } ;
foreach ( var ( mass , at ) in bodies )
{
var orbit = _cache . Upsert ( new OrbitData { FixedPosition = at } ) . Key ;
2026-10-15 06:36:02 +02:00
var data = new PlanetData { Name = "Body " + + + _bodies , Orbit = new CultRecordRef < OrbitData > ( orbit ) , Mass = mass } ;
author ? . Invoke ( data ) ;
var body = _cache . Upsert ( data ) . Key ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
pack . Orbits . Add ( new CultRecordRef < OrbitData > ( orbit ) ) ;
pack . Planets . Add ( new CultRecordRef < BodyData > ( body ) ) ;
}
return new Zone ( _items , planets , pack , new GalaxyZone { Name = "Potential " + _bodies , Owner = null } , null ) ;
}
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 ) ;
2026-10-16 17:58:17 +02:00
// 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 ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
2026-10-15 05:02:48 +02:00
// A zone whose bodies have no authored gravity curves (most test zones) has no pull and no dip, and no NaN either.
[Fact]
public void BodiesWithNoAuthoredWellPullAndDigNothing ( )
{
var planets = new PlanetSettings
{
2026-10-15 07:11:25 +02:00
BodyRadius = new ExponentialCurve ( ) ,
2026-10-15 18:11:27 +02:00
LightRadius = new ExponentialCurve ( )
2026-10-15 05:02:48 +02:00
} ;
var zone = ZoneOf ( planets , ( 100f , float2 . zero ) , ( 5000f , new float2 ( 200f , 0f ) ) ) ;
foreach ( var p in new [ ] { float2 . zero , float2 ( 200f , 0f ) , float2 ( 37f , - 911f ) , float2 ( 5000f , 5000f ) } )
{
Assert . Equal ( 0f , zone . GetHeight ( p ) ) ;
Assert . Equal ( 0f , zone . GetForce ( p ) . x ) ;
Assert . Equal ( 0f , zone . GetForce ( p ) . y ) ;
}
}
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
[Fact]
public void StrengthFollowsTheAuthoredMassCurve ( )
{
var planets = Curves ( ) ;
foreach ( var mass in new [ ] { 29.2f , 273.9f , 14152.4f } )
{
var zone = ZoneOf ( planets , ( mass , float2 . zero ) ) ;
2026-10-15 16:46:53 +02:00
var core = planets . RocheLimit . Evaluate ( mass ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
foreach ( var r in new [ ] { 3f * core , 10f * core , PackRadius / 2f } )
2026-10-16 17:58:17 +02:00
Near ( planets . WellStrength . Evaluate ( mass ) * r * r / ( r * r + core * core ) , length ( zone . GetForce ( new float2 ( r , 0 ) ) ) * r , 1e-3f ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
}
}
2026-10-16 17:58:17 +02:00
// The steepest pull of the Plummer well is at its softening, which is the Roche curve: it rises up to it and falls beyond.
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
[Fact]
2026-10-16 17:58:17 +02:00
public void TheSteepestPullIsAtTheRocheCurve ( )
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
{
var planets = Curves ( ) ;
foreach ( var mass in new [ ] { 29.2f , 14152.4f } )
{
var zone = ZoneOf ( planets , ( mass , float2 . zero ) ) ;
2026-10-15 16:46:53 +02:00
var core = planets . RocheLimit . Evaluate ( mass ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
float Pull ( float r ) = > length ( zone . GetForce ( new float2 ( 0 , r ) ) ) ;
2026-10-16 17:58:17 +02:00
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" ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
Assert . True ( Pull ( core ) > Pull ( 1.02f * core ) , "and falls beyond it" ) ;
}
}
2026-10-15 16:46:53 +02:00
// core-at-roche-and-exaggeration-up: the one curve is both the Roche limit and the core. A body with a well and no
// Roche limit has no core and is an authoring error; any positive, finite limit builds.
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
[Fact]
2026-10-15 16:46:53 +02:00
public void TheRocheLimitIsTheCore ( )
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
{
var planets = Curves ( ) ;
foreach ( var mass in new [ ] { 29.2f , 14152.4f } )
Near ( planets . RocheLimit . Evaluate ( mass ) , ZoneOf ( planets , ( mass , float2 . zero ) ) . PlanetInstances . Values . Single ( ) . RocheLimit , 1e-6f ) ;
2026-10-15 16:46:53 +02:00
PlanetSettings Constants ( float roche ) = > new PlanetSettings
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
{
WellStrength = new ExponentialCurve { Constant = 100f } ,
RocheLimit = new ExponentialCurve { Constant = roche } ,
BodyRadius = new ExponentialCurve { Constant = 10f } ,
2026-10-15 18:11:27 +02:00
LightRadius = new ExponentialCurve ( )
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
} ;
2026-10-15 16:46:53 +02:00
Assert . Throws < InvalidOperationException > ( ( ) = > ZoneOf ( Constants ( 0f ) , ( 100f , float2 . zero ) ) ) ;
Assert . Throws < InvalidOperationException > ( ( ) = > ZoneOf ( Constants ( - 5f ) , ( 100f , float2 . zero ) ) ) ;
Assert . Throws < InvalidOperationException > ( ( ) = > ZoneOf ( Constants ( float . PositiveInfinity ) , ( 100f , float2 . zero ) ) ) ;
ZoneOf ( Constants ( 50f ) , ( 100f , float2 . zero ) ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
}
2026-10-16 17:58:17 +02:00
// 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.
2026-10-15 16:46:53 +02:00
[Fact]
public void ThePeakDrawnSlopeStaysAndTheFunnelDrawsSteeper ( )
2026-10-15 06:36:02 +02:00
{
2026-10-15 16:46:53 +02:00
const float oldCoreMultiplier = 3f , oldExaggeration = 10f ;
var planets = AuthoredSettings . Load ( RunStartTests . FindRepoRoot ( ) ) . Read < PlanetSettings > ( "PlanetSettings" ) ;
foreach ( var mass in new [ ] { 29.2f , 273.9f , 14152.4f } )
2026-10-15 06:36:02 +02:00
{
2026-10-15 16:46:53 +02:00
var zone = ZoneOf ( planets , ( mass , float2 . zero ) ) ;
var c = planets . WellStrength . Evaluate ( mass ) ;
var oldCore = oldCoreMultiplier * pow ( mass , . 25f ) ;
float Drawn ( float r ) = > length ( zone . GetForce ( new float2 ( r , 0 ) ) ) / zone . Settings . HeightExaggeration ;
var peakBefore = c / oldCore / oldExaggeration ;
Assert . InRange ( Drawn ( planets . RocheLimit . Evaluate ( mass ) ) , peakBefore * . 98f , peakBefore * 1.02f ) ;
var farBefore = c / ( 3f * oldCore ) / oldExaggeration ;
2026-10-16 17:58:17 +02:00
Assert . InRange ( Drawn ( 3f * oldCore ) / farBefore , 2.9f , 2.98f ) ;
2026-10-15 06:36:02 +02:00
}
}
2026-10-15 16:46:53 +02:00
// The shipped curves build a well at every mass of the generated range. The curves are the ones
2026-10-15 07:23:26 +02:00
// Settings.asset carries, read the way RunStartTests reads them, so a retune of the asset is judged here.
2026-10-15 06:36:02 +02:00
[Fact]
public void TheAuthoredCurvesBuildAtEveryMass ( )
{
2026-10-15 07:23:26 +02:00
var planets = AuthoredSettings . Load ( RunStartTests . FindRepoRoot ( ) ) . Read < PlanetSettings > ( "PlanetSettings" ) ;
2026-10-15 16:46:53 +02:00
Assert . True ( planets . WellStrength . Evaluate ( 100f ) > 0f & & planets . RocheLimit . Evaluate ( 100f ) > 0f , "the authored asset carries a well" ) ;
2026-10-15 06:36:02 +02:00
for ( var exponent10 = - 3f ; exponent10 < = 8f ; exponent10 + = . 25f )
ZoneOf ( planets , ( pow ( 10f , exponent10 ) , float2 . zero ) ) ;
}
// A well that is negative, not a number or infinite is an authoring error, not an antimass body. Zero is the unauthored
// default and digs nothing (BodiesWithNoAuthoredWellPullAndDigNothing).
[Theory]
[InlineData(-10f)]
[InlineData(float.NaN)]
[InlineData(float.PositiveInfinity)]
[InlineData(float.NegativeInfinity)]
2026-10-15 07:23:26 +02:00
[InlineData(-1e-6f)]
[InlineData(-float.Epsilon)]
2026-10-15 06:36:02 +02:00
public void AnUnphysicalWellStrengthThrowsAtConstruction ( float strength )
{
var planets = Curves ( ) ;
planets . WellStrength = new ExponentialCurve { Constant = strength } ;
Assert . Throws < InvalidOperationException > ( ( ) = > ZoneOf ( planets , ( 100f , float2 . zero ) ) ) ;
planets . RocheLimit = new ExponentialCurve ( ) ;
Assert . Throws < InvalidOperationException > ( ( ) = > ZoneOf ( planets , ( 100f , float2 . zero ) ) ) ;
}
2026-10-15 16:46:53 +02:00
// Zero is the boundary: a well of no strength builds beside the authored Roche curve (it digs nothing), where
2026-10-15 07:23:26 +02:00
// the smallest negative strength does not.
[Theory]
[InlineData(0f)]
[InlineData(-0f)]
public void AWellOfNoStrengthBuildsAndDigsNothing ( float strength )
{
var planets = Curves ( ) ;
planets . WellStrength = new ExponentialCurve { Constant = strength } ;
var zone = ZoneOf ( planets , ( 100f , float2 . zero ) ) ;
foreach ( var p in new [ ] { float2 . zero , float2 ( 3f , 1f ) , float2 ( 40f , - 9f ) , float2 ( 900f , 0f ) } )
{
Assert . Equal ( 0f , zone . GetHeight ( p ) ) ;
Assert . Equal ( 0f , zone . GetForce ( p ) . x ) ;
Assert . Equal ( 0f , zone . GetForce ( p ) . y ) ;
}
}
2026-10-15 06:36:02 +02:00
// The well is authored once, by curves of mass (target: no per-body authoring of a core or Roche limit): a body whose
2026-10-15 07:11:25 +02:00
// record carries another radius multiplier has the same strength, core and Roche limit, and pulls and digs exactly as
// its plain twin.
2026-10-15 06:36:02 +02:00
[Theory]
2026-10-15 07:11:25 +02:00
[InlineData(2.5f)]
[InlineData(.5f)]
public void PerBodyMultipliersDoNotMoveTheWell ( float bodyRadius )
2026-10-15 06:36:02 +02:00
{
var planets = Curves ( ) ;
foreach ( var mass in new [ ] { 29.2f , 14152.4f } )
{
var plain = ZoneOf ( planets , ( mass , float2 . zero ) ) ;
var scaled = ZoneOf ( planets , data = >
{
data . BodyRadiusMultiplier = bodyRadius ;
} , ( mass , float2 . zero ) ) ;
var a = plain . PlanetInstances . Values . Single ( ) ;
var b = scaled . PlanetInstances . Values . Single ( ) ;
Assert . Equal ( a . WellStrength , b . WellStrength ) ;
Assert . Equal ( a . RocheLimit , b . RocheLimit ) ;
2026-10-15 16:46:53 +02:00
foreach ( var p in new [ ] { float2 ( . 4f * a . RocheLimit , 0 ) , float2 ( 0 , 3f * a . RocheLimit ) , float2 ( 400f , 300f ) } )
2026-10-15 06:36:02 +02:00
{
Assert . Equal ( plain . GetForce ( p ) , scaled . GetForce ( p ) ) ;
Assert . Equal ( plain . GetHeight ( p ) , scaled . GetHeight ( p ) ) ;
}
}
}
2026-10-16 17:58:17 +02:00
// A lone body is a global term: it pulls as the Plummer well at every range, with no rim and no gate.
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
[Fact]
2026-10-16 17:58:17 +02:00
public void TheGlobalTermIsPlummerAtEveryRange ( )
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
{
var planets = Curves ( ) ;
var zone = ZoneOf ( planets , ( 273.9f , float2 . zero ) ) ;
var c = planets . WellStrength . Evaluate ( 273.9f ) ;
2026-10-15 16:46:53 +02:00
var core = planets . RocheLimit . Evaluate ( 273.9f ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
foreach ( var r in new [ ] { core , 2f * core , PackRadius , 10f * PackRadius , 100f * PackRadius , 1000f * PackRadius } )
foreach ( var angle in new [ ] { 0f , 1.1f , 3.5f } )
{
var at = new float2 ( r * cos ( angle ) , r * sin ( angle ) ) ;
var force = zone . GetForce ( at ) ;
2026-10-16 17:58:17 +02:00
Near ( c * r * r / ( r * r + core * core ) , length ( force ) * r , 1e-3f ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
Assert . True ( dot ( normalize ( force ) , normalize ( - at ) ) > . 9999f , "the pull points at the body" ) ;
}
}
[Fact]
public void HeightIsThePotentialOverOneK ( )
{
foreach ( var k in new [ ] { 1f , 10f } )
{
var planets = Curves ( k ) ;
var lone = ZoneOf ( planets , ( 273.9f , float2 . zero ) ) ;
var c = planets . WellStrength . Evaluate ( 273.9f ) ;
2026-10-15 16:46:53 +02:00
var core = planets . RocheLimit . Evaluate ( 273.9f ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
Assert . Equal ( 0f , lone . GetHeight ( new float2 ( PackRadius , 0 ) ) , 4 ) ;
2026-10-16 17:58:17 +02:00
Near ( - c * . 5f * log ( ( PackRadius * PackRadius + core * core ) / ( core * core ) ) / k , lone . GetHeight ( float2 . zero ) , 1e-4f ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
var two = ZoneOf ( planets , ( 29.2f , new float2 ( - 300f , 0f ) ) , ( 14152.4f , new float2 ( 300f , 0f ) ) ) ;
var c0 = planets . WellStrength . Evaluate ( 29.2f ) ;
2026-10-15 16:46:53 +02:00
var core0 = planets . RocheLimit . Evaluate ( 29.2f ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
var c1 = planets . WellStrength . Evaluate ( 14152.4f ) ;
2026-10-15 16:46:53 +02:00
var core1 = planets . RocheLimit . Evaluate ( 14152.4f ) ;
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
const float step = . 5f ;
2026-10-16 17:58:17 +02:00
// Inside the two softenings as well, where the Plummer potential is smooth through the centre.
2026-10-15 06:36:02 +02:00
var inCore = new [ ]
{
float2 ( - 300f + . 5f * core0 , 0f ) , float2 ( - 300f , . 65f * core0 ) , float2 ( - 300f - . 86f * core0 , 0f ) , float2 ( - 300f + . 6f * core0 , . 6f * core0 ) ,
float2 ( 300f + . 55f * core1 , 0f ) , float2 ( 300f , - . 7f * core1 ) , float2 ( 300f - . 86f * core1 , 0f ) , float2 ( 300f + . 9f * core1 , 0f )
} ;
2026-10-16 21:32:30 +02:00
// Two suns 600 apart are two subzones: each is felt only inside its own reach (half the distance, 300), so the height
// there is that well's alone (plus the zone node's constant, which differences cancel) and the force is its alone.
var w0 = float2 ( - 300f , 0f ) ;
var w1 = float2 ( 300f , 0f ) ;
foreach ( var p in inCore . Concat ( new [ ] { float2 ( - 200f , 120f ) , float2 ( 100f , - 300f ) , float2 ( 500f , 40f ) , float2 ( - 290f , 12f ) , float2 ( 310f , 20f ) , float2 ( 700f , 700f ) , float2 ( - 80f , - 500f ) } ) )
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
{
2026-10-16 21:51:20 +02:00
var near0 = length ( p - w0 ) < 140f ; // inside Gamma x reach (150) the well is its Plummer pull whole
var near1 = length ( p - w1 ) < 140f ;
2026-10-16 21:32:30 +02:00
if ( near0 | | near1 )
{
var ( at , wc , wcore ) = near0 ? ( w0 , c0 , core0 ) : ( w1 , c1 , core1 ) ;
var expected = Phi ( wc , wcore , length ( p - at ) ) - Phi ( wc , wcore , 0f ) ;
Near ( expected , ( two . GetHeight ( p ) - two . GetHeight ( at ) ) * k , 1e-3f ) ;
}
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
// The downhill slope of the potential is the force: the central difference of height * K is -GetForce.
var gradient = float2 (
( two . GetHeight ( p + float2 ( step , 0 ) ) - two . GetHeight ( p - float2 ( step , 0 ) ) ) * k / ( 2 * step ) ,
( two . GetHeight ( p + float2 ( 0 , step ) ) - two . GetHeight ( p - float2 ( 0 , step ) ) ) * k / ( 2 * step ) ) ;
var force = two . GetForce ( p ) ;
2026-10-16 21:34:34 +02:00
Assert . InRange ( length ( gradient + force ) , 0f , length ( force ) * 1e-2f + 1e-4f + 3e-5f * k ) ; // flat ground between the subzones reads the float32 resolution of the height, times K
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
}
}
}
}