Every rail's period is derived from the field it orbits in; OrbitPeriod is deleted
Zone.PeriodAround is the one owner: round a body, the pull its well gives at the rail's distance (GravityField.Acceleration); round a bodyless node, the ring law (strength of the orbits inside, plus half of what shares the ring). Orbit.Period and AsteroidBelt.Periods are written once at zone build, before placement; BeltTargets copies the belt's periods. A parentless orbit has no field and stands at its phase. Fixtures lose OrbitPeriod and gain wells where their rails must move. Builds; the new OrbitsFromFieldTests follow in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8f491fd3c9
commit
a4977cbe48
13 changed files with 151 additions and 61 deletions
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
Binary file not shown.
|
|
@ -31,7 +31,6 @@ public class PlanetSettings
|
||||||
public ExponentialLerp AsteroidRespawnTime;
|
public ExponentialLerp AsteroidRespawnTime;
|
||||||
public float SecureAreaRadiusMultiplier = .45f;
|
public float SecureAreaRadiusMultiplier = .45f;
|
||||||
|
|
||||||
public ExponentialCurve OrbitPeriod;
|
|
||||||
|
|
||||||
// The global flow stardust and nebula follow and the sim knows (FlowField). Zero flow by default.
|
// The global flow stardust and nebula follow and the sim knows (FlowField). Zero flow by default.
|
||||||
public FlowSettings Flow = new FlowSettings();
|
public FlowSettings Flow = new FlowSettings();
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ public sealed class BeltTargets : ITargetProvider
|
||||||
{
|
{
|
||||||
order[i] = i;
|
order[i] = i;
|
||||||
distances[i] = data.Asteroids[i].Distance;
|
distances[i] = data.Asteroids[i].Distance;
|
||||||
// Pose divides by the float period the curve returns; the keys must use the same one.
|
// The belt's own periods (Zone.PeriodAround), the very floats Pose divides by.
|
||||||
_periods[i] = settings.OrbitPeriod.Evaluate(distances[i]);
|
_periods[i] = zone.AsteroidBelts[field].Periods[i];
|
||||||
}
|
}
|
||||||
Array.Sort(distances, order);
|
Array.Sort(distances, order);
|
||||||
_bands = new Band[(count + RocksPerBand - 1) / RocksPerBand];
|
_bands = new Band[(count + RocksPerBand - 1) / RocksPerBand];
|
||||||
|
|
|
||||||
|
|
@ -158,15 +158,9 @@ public class Zone
|
||||||
|
|
||||||
foreach (var orbit in pack.Orbits)
|
foreach (var orbit in pack.Orbits)
|
||||||
{
|
{
|
||||||
Orbits.Add(orbit.Key, new Orbit(Settings, cache.Get(orbit)));
|
Orbits.Add(orbit.Key, new Orbit(cache.Get(orbit)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aetheria gravity (target invariant zone-valid-before-tick): every orbit is placed at the pack's time
|
|
||||||
// when the zone is built, so each well, light and body pose read before the first Update is where the
|
|
||||||
// body is. Velocity stays zero until the first Update differences two placed positions.
|
|
||||||
foreach (var orbit in Orbits)
|
|
||||||
GetOrbitPosition(orbit.Key);
|
|
||||||
|
|
||||||
foreach (var body in pack.Planets)
|
foreach (var body in pack.Planets)
|
||||||
{
|
{
|
||||||
var planet = cache.Get(body);
|
var planet = cache.Get(body);
|
||||||
|
|
@ -178,7 +172,6 @@ public class Zone
|
||||||
// the rule generation uses, and keeps it.
|
// the rule generation uses, and keeps it.
|
||||||
FieldKinds.Ensure(body.Key, belt, cache);
|
FieldKinds.Ensure(body.Key, belt, cache);
|
||||||
AsteroidBelts[body.Key] = new AsteroidBelt(belt);
|
AsteroidBelts[body.Key] = new AsteroidBelt(belt);
|
||||||
Targets.Add(new BeltTargets(this, body.Key, belt, settings, _time));
|
|
||||||
break;
|
break;
|
||||||
case SunData sun:
|
case SunData sun:
|
||||||
var sunInstance = new Sun(settings, sun, Orbits[planet.Orbit.Key]);
|
var sunInstance = new Sun(settings, sun, Orbits[planet.Orbit.Key]);
|
||||||
|
|
@ -194,6 +187,40 @@ public class Zone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Orbits, bodies and belts exist: every rail's period is derived once, from the field it orbits in.
|
||||||
|
foreach (var orbit in Orbits)
|
||||||
|
{
|
||||||
|
var parent = orbit.Value.Data.Parent;
|
||||||
|
if (!parent.IsSet()) continue;
|
||||||
|
if (!_childOrbits.TryGetValue(parent.Key, out var siblings))
|
||||||
|
_childOrbits[parent.Key] = siblings = new List<CultRecordKey>();
|
||||||
|
siblings.Add(orbit.Key);
|
||||||
|
}
|
||||||
|
foreach (var body in PlanetInstances)
|
||||||
|
if (body.Value.WellStrength > 0)
|
||||||
|
_wellOn.TryAdd(Planets[body.Key].Orbit.Key, body.Value);
|
||||||
|
foreach (var orbit in Orbits)
|
||||||
|
orbit.Value.Period = orbit.Value.Data.Parent.IsSet()
|
||||||
|
? PeriodAround(orbit.Value.Data.Parent.Key, orbit.Value.Data.Distance, orbit.Key)
|
||||||
|
: float.PositiveInfinity;
|
||||||
|
foreach (var belt in AsteroidBelts)
|
||||||
|
{
|
||||||
|
var beltOrbit = Planets[belt.Key].Orbit.Key;
|
||||||
|
var beltParent = Orbits[beltOrbit].Data.Parent.Key;
|
||||||
|
for (var i = 0; i < belt.Value.Periods.Length; i++)
|
||||||
|
belt.Value.Periods[i] = PeriodAround(beltParent, belt.Value.Data.Asteroids[i].Distance, beltOrbit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aetheria gravity (target invariant zone-valid-before-tick): every orbit is placed at the pack's time
|
||||||
|
// when the zone is built, so each well, light and body pose read before the first Update is where the
|
||||||
|
// body is. Velocity stays zero until the first Update differences two placed positions.
|
||||||
|
foreach (var orbit in Orbits)
|
||||||
|
GetOrbitPosition(orbit.Key);
|
||||||
|
|
||||||
|
// BeltTargets copies the belts' periods, so it is built after them.
|
||||||
|
foreach (var belt in AsteroidBelts)
|
||||||
|
Targets.Add(new BeltTargets(this, belt.Key, belt.Value.Data, settings, _time));
|
||||||
|
|
||||||
// Cut 2 (docs/mining-cut.md): read after belts are built so a later ChunkExists/ChunkRadius call has
|
// Cut 2 (docs/mining-cut.md): read after belts are built so a later ChunkExists/ChunkRadius call has
|
||||||
// Planets/AsteroidBelts populated. A pack from before key 6 existed has ChunkWear == null (nullable
|
// Planets/AsteroidBelts populated. A pack from before key 6 existed has ChunkWear == null (nullable
|
||||||
// persistence rule); that reads as no wear rather than throwing.
|
// persistence rule); that reads as no wear rather than throwing.
|
||||||
|
|
@ -481,6 +508,8 @@ public class Zone
|
||||||
{
|
{
|
||||||
var orbit = Orbits[orbitID];
|
var orbit = Orbits[orbitID];
|
||||||
float2 pos = float2.zero;
|
float2 pos = float2.zero;
|
||||||
|
// An infinite period (a parentless orbit, or one with no field to circle in) leaves the orbit at its phase:
|
||||||
|
// frac(t / inf) is 0.
|
||||||
if (orbit.Period > .01f)
|
if (orbit.Period > .01f)
|
||||||
{
|
{
|
||||||
var phase = (float) frac(_time / orbit.Period);
|
var phase = (float) frac(_time / orbit.Period);
|
||||||
|
|
@ -503,6 +532,52 @@ public class Zone
|
||||||
return Orbits[orbitID].Position;
|
return Orbits[orbitID].Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// orbit -> the well-bearing body on it, and orbit -> the orbits whose parent it is. Derived from the pack in the
|
||||||
|
// constructor, read only by PeriodAround and Strength.
|
||||||
|
private readonly Dictionary<CultRecordKey, Planet> _wellOn = new Dictionary<CultRecordKey, Planet>();
|
||||||
|
private readonly Dictionary<CultRecordKey, List<CultRecordKey>> _childOrbits = new Dictionary<CultRecordKey, List<CultRecordKey>>();
|
||||||
|
private readonly Dictionary<CultRecordKey, float> _strengths = new Dictionary<CultRecordKey, float>();
|
||||||
|
|
||||||
|
// The one owner of a rail's period (Orbit.Period, AsteroidBelt.Periods): 2 pi sqrt(d / a), a being the pull the
|
||||||
|
// field gives at distance d from the node being orbited. Round a body, that is the body's own well through
|
||||||
|
// GravityField. Round a bodyless node it is the ring law: the strength of the node's orbits inside d, plus half
|
||||||
|
// that of the ones on d itself (a companion on the ring pulls half, as a binary does), taken as a point at the
|
||||||
|
// node. `self` is the orbit being placed (excluded from its own pull); a strengthless self takes no ring share.
|
||||||
|
// No pull gives an infinite period.
|
||||||
|
private float PeriodAround(CultRecordKey parentOrbit, float distance, CultRecordKey self)
|
||||||
|
{
|
||||||
|
float a;
|
||||||
|
if (_wellOn.TryGetValue(parentOrbit, out var body))
|
||||||
|
a = length(GravityField.Acceleration(body.WellStrength, body.RocheLimit, float2(distance, 0)));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var inside = 0f;
|
||||||
|
var onRing = 0f;
|
||||||
|
if (_childOrbits.TryGetValue(parentOrbit, out var children))
|
||||||
|
foreach (var child in children)
|
||||||
|
{
|
||||||
|
if (child.Equals(self)) continue;
|
||||||
|
var childDistance = Orbits[child].Data.Distance;
|
||||||
|
if (childDistance < distance - 0.1f) inside += Strength(child);
|
||||||
|
else if (childDistance <= distance + 0.1f) onRing += Strength(child);
|
||||||
|
}
|
||||||
|
var strength = inside + (Strength(self) > 0 ? onRing / 2 : 0f);
|
||||||
|
a = length(GravityField.Acceleration(strength, 0f, float2(distance, 0)));
|
||||||
|
}
|
||||||
|
return a > 0 && distance > 0 ? 2 * PI * sqrt(distance / a) : float.PositiveInfinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The well strength an orbit carries as a node: its own body's plus every orbit round it, recursively.
|
||||||
|
private float Strength(CultRecordKey orbit)
|
||||||
|
{
|
||||||
|
if (_strengths.TryGetValue(orbit, out var known)) return known;
|
||||||
|
var strength = _wellOn.TryGetValue(orbit, out var body) ? body.WellStrength : 0f;
|
||||||
|
if (_childOrbits.TryGetValue(orbit, out var children))
|
||||||
|
foreach (var child in children)
|
||||||
|
strength += Strength(child);
|
||||||
|
return _strengths[orbit] = strength;
|
||||||
|
}
|
||||||
|
|
||||||
public float2 GetOrbitVelocity(CultRecordKey orbit)
|
public float2 GetOrbitVelocity(CultRecordKey orbit)
|
||||||
{
|
{
|
||||||
if (Orbits.ContainsKey(orbit))
|
if (Orbits.ContainsKey(orbit))
|
||||||
|
|
@ -571,7 +646,7 @@ public class Zone
|
||||||
var beltData = Planets[belt] as AsteroidBeltData;
|
var beltData = Planets[belt] as AsteroidBeltData;
|
||||||
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
||||||
var size = ChunkRadius(new ChunkId(belt, index));
|
var size = ChunkRadius(new ChunkId(belt, index));
|
||||||
return AsteroidBelts[belt].Pose(index, _time, parentPosition, Settings, size);
|
return AsteroidBelts[belt].Pose(index, _time, parentPosition, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mining Cut 3 (docs/mining-cut-refresh.md): how bright a chunk is -- Reflector's rule (cross-section × the light
|
// Mining Cut 3 (docs/mining-cut-refresh.md): how bright a chunk is -- Reflector's rule (cross-section × the light
|
||||||
|
|
@ -592,7 +667,7 @@ public class Zone
|
||||||
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
||||||
var beltInstance = AsteroidBelts[belt];
|
var beltInstance = AsteroidBelts[belt];
|
||||||
for (var i = 0; i < beltData.Asteroids.Length; i++)
|
for (var i = 0; i < beltData.Asteroids.Length; i++)
|
||||||
into[i] = beltInstance.Pose(i, _time, parentPosition, Settings, ChunkRadius(new ChunkId(belt, i)));
|
into[i] = beltInstance.Pose(i, _time, parentPosition, ChunkRadius(new ChunkId(belt, i)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SecurityLevel GetSecurityLevel(float2 pos)
|
public SecurityLevel GetSecurityLevel(float2 pos)
|
||||||
|
|
@ -748,9 +823,14 @@ public class AsteroidBelt
|
||||||
public AsteroidBeltData Data;
|
public AsteroidBeltData Data;
|
||||||
public float Radius { get; }
|
public float Radius { get; }
|
||||||
|
|
||||||
|
// Each asteroid's orbital period, in seconds: derived once at zone build by Zone.PeriodAround (the field at the
|
||||||
|
// asteroid's own distance from the belt's parent), the only period Pose and BeltTargets divide by.
|
||||||
|
public float[] Periods { get; }
|
||||||
|
|
||||||
public AsteroidBelt(AsteroidBeltData data)
|
public AsteroidBelt(AsteroidBeltData data)
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
|
Periods = new float[data.Asteroids.Length];
|
||||||
Radius = data.Asteroids.Max(a => a.Distance);
|
Radius = data.Asteroids.Max(a => a.Distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -758,12 +838,11 @@ public class AsteroidBelt
|
||||||
// The only place a chunk pose is computed; no per-tick copy survives it. Cut 2: size is no longer read from
|
// The only place a chunk pose is computed; no per-tick copy survives it. Cut 2: size is no longer read from
|
||||||
// per-belt storage -- Zone owns wear now, so the caller (Zone.ChunkPose/EvaluateBelt) computes it through
|
// per-belt storage -- Zone owns wear now, so the caller (Zone.ChunkPose/EvaluateBelt) computes it through
|
||||||
// Zone.ChunkRadius and passes it in.
|
// Zone.ChunkRadius and passes it in.
|
||||||
public float4 Pose(int index, double time, float2 parentPosition, PlanetSettings settings, float size)
|
public float4 Pose(int index, double time, float2 parentPosition, float size)
|
||||||
{
|
{
|
||||||
var asteroid = Data.Asteroids[index];
|
var asteroid = Data.Asteroids[index];
|
||||||
var rot = (float) (time * asteroid.RotationSpeed % (PI * 2));
|
var rot = (float) (time * asteroid.RotationSpeed % (PI * 2));
|
||||||
var pos = OrbitData.Evaluate((float) frac(time / settings.OrbitPeriod.Evaluate(asteroid.Distance) +
|
var pos = OrbitData.Evaluate((float) frac(time / Periods[index] + asteroid.Phase)) * asteroid.Distance + parentPosition;
|
||||||
asteroid.Phase)) * asteroid.Distance + parentPosition;
|
|
||||||
return float4(pos.x, pos.y, rot, size);
|
return float4(pos.x, pos.y, rot, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -845,11 +924,13 @@ public class Orbit
|
||||||
public float2 Velocity = float2.zero;
|
public float2 Velocity = float2.zero;
|
||||||
public float2 Position = float2.zero;
|
public float2 Position = float2.zero;
|
||||||
public float2 PreviousPosition = float2.zero;
|
public float2 PreviousPosition = float2.zero;
|
||||||
public float Period;
|
|
||||||
|
|
||||||
public Orbit(PlanetSettings settings, OrbitData data)
|
// Seconds per turn, written only by the Zone constructor (Zone.PeriodAround); infinite when the orbit has no
|
||||||
|
// field to circle in.
|
||||||
|
public float Period { get; internal set; }
|
||||||
|
|
||||||
|
public Orbit(OrbitData data)
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
Period = settings.OrbitPeriod.Evaluate(data.Distance);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -531,7 +531,6 @@ public class GeneratorPlanet
|
||||||
public float Distance;
|
public float Distance;
|
||||||
public float Phase;
|
public float Phase;
|
||||||
public float Mass;
|
public float Mass;
|
||||||
//public float Period;
|
|
||||||
public float ChildDistanceMinimum;
|
public float ChildDistanceMinimum;
|
||||||
public float ChildDistanceMaximum;
|
public float ChildDistanceMaximum;
|
||||||
public bool Empty = false;
|
public bool Empty = false;
|
||||||
|
|
@ -586,7 +585,6 @@ public class GeneratorPlanet
|
||||||
ChildDistanceMaximum = (i % 2 == 0 ? p0ChildDist : p1ChildDist)
|
ChildDistanceMaximum = (i % 2 == 0 ? p0ChildDist : p1ChildDist)
|
||||||
};
|
};
|
||||||
child.ChildDistanceMinimum = Settings.PlanetSafetyRadius.Evaluate(child.Mass) * 2;
|
child.ChildDistanceMinimum = Settings.PlanetSafetyRadius.Evaluate(child.Mass) * 2;
|
||||||
//child.Period = Settings..Evaluate(child.Distance);
|
|
||||||
Children.Add(child);
|
Children.Add(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -647,7 +645,6 @@ public class GeneratorPlanet
|
||||||
Distance = distances[i],
|
Distance = distances[i],
|
||||||
Phase = random.NextFloat()
|
Phase = random.NextFloat()
|
||||||
};
|
};
|
||||||
//child.Period = Context.GlobalData.OrbitalPeriod(child.Distance);
|
|
||||||
child.ChildDistanceMinimum = Settings.PlanetSafetyRadius.Evaluate(child.Mass) * 2;
|
child.ChildDistanceMinimum = Settings.PlanetSafetyRadius.Evaluate(child.Mass) * 2;
|
||||||
// Maximum child distance of child is the smallest distance to either of its neighbors
|
// Maximum child distance of child is the smallest distance to either of its neighbors
|
||||||
child.ChildDistanceMaximum = min(i == 0 ? child.Distance - ChildDistanceMinimum : child.Distance - distances[i - 1],
|
child.ChildDistanceMaximum = min(i == 0 ? child.Distance - ChildDistanceMinimum : child.Distance - distances[i - 1],
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,7 @@ public sealed class GravitySubtractTests : IDisposable
|
||||||
WellStrength = new ExponentialCurve { Exponent = .2f, Multiplier = 45.08f },
|
WellStrength = new ExponentialCurve { Exponent = .2f, Multiplier = 45.08f },
|
||||||
RocheLimit = new ExponentialCurve { Exponent = .25f, Multiplier = 7.32f },
|
RocheLimit = new ExponentialCurve { Exponent = .25f, Multiplier = 7.32f },
|
||||||
BodyRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 3f },
|
BodyRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 3f },
|
||||||
LightRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 300f },
|
LightRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 300f }
|
||||||
OrbitPeriod = new ExponentialCurve()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private Zone Bare(float packRadius) => new Zone(_items, Planets(), new ZonePack { Radius = packRadius },
|
private Zone Bare(float packRadius) => new Zone(_items, Planets(), new ZonePack { Radius = packRadius },
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,17 @@ public sealed class MiningCut1Tests : IDisposable
|
||||||
|
|
||||||
// Authored-shape curves, not defaults: a default ExponentialLerp/ExponentialCurve evaluates to a flat
|
// Authored-shape curves, not defaults: a default ExponentialLerp/ExponentialCurve evaluates to a flat
|
||||||
// constant everywhere, which would hide a broken formula (every asteroid or every damage state would read
|
// constant everywhere, which would hide a broken formula (every asteroid or every damage state would read
|
||||||
// alike). These give period, size and hitpoints real, distinct curvature over the fixture's distance and
|
// alike). These give size and hitpoints real, distinct curvature over the fixture's damage range. A body's
|
||||||
// damage range.
|
// well strength is its mass here (c = mass), with a small constant core, so the test spells every period from
|
||||||
|
// the masses it chose.
|
||||||
|
private const float SunStrength = 400f;
|
||||||
|
private const float PlanetStrength = 25f;
|
||||||
|
|
||||||
private static PlanetSettings Settings() => new PlanetSettings
|
private static PlanetSettings Settings() => new PlanetSettings
|
||||||
{
|
{
|
||||||
OrbitPeriod = new ExponentialCurve { Multiplier = 0.6f, Exponent = 1.3f, Constant = 40f },
|
BodyRadius = new ExponentialCurve(),
|
||||||
|
WellStrength = new ExponentialCurve { Multiplier = 1f, Exponent = 1f },
|
||||||
|
RocheLimit = new ExponentialCurve { Constant = 2f },
|
||||||
AsteroidSize = new ExponentialLerp { Minimum = 2f, Maximum = 9f, Exponent = 1.7f },
|
AsteroidSize = new ExponentialLerp { Minimum = 2f, Maximum = 9f, Exponent = 1.7f },
|
||||||
AsteroidHitpoints = new ExponentialLerp { Minimum = 10f, Maximum = 300f, Exponent = 2f },
|
AsteroidHitpoints = new ExponentialLerp { Minimum = 10f, Maximum = 300f, Exponent = 2f },
|
||||||
AsteroidRespawnTime = new ExponentialLerp { Minimum = 5f, Maximum = 60f, Exponent = 1f }
|
AsteroidRespawnTime = new ExponentialLerp { Minimum = 5f, Maximum = 60f, Exponent = 1f }
|
||||||
|
|
@ -48,7 +54,7 @@ public sealed class MiningCut1Tests : IDisposable
|
||||||
private const float RootPhase = 0.17f;
|
private const float RootPhase = 0.17f;
|
||||||
|
|
||||||
// A belt of AsteroidCount asteroids at distinct distances, phases, size parameters and rotation speeds,
|
// A belt of AsteroidCount asteroids at distinct distances, phases, size parameters and rotation speeds,
|
||||||
// orbiting a parent (RootDistance/RootPhase) that is itself moving -- so a test that reads the parent's
|
// orbiting a planet (RootDistance/RootPhase round a fixed sun) that is itself moving -- so a test that reads the parent's
|
||||||
// motion through ChunkPose is exercising real recursion, not a fixed point. Each call opens its own
|
// motion through ChunkPose is exercising real recursion, not a fixed point. Each call opens its own
|
||||||
// catalog, so two zones built from this fixture never share cache state.
|
// catalog, so two zones built from this fixture never share cache state.
|
||||||
private (Zone zone, CultRecordKey beltKey, Asteroid[] asteroids, PlanetSettings settings) BuildFixture()
|
private (Zone zone, CultRecordKey beltKey, Asteroid[] asteroids, PlanetSettings settings) BuildFixture()
|
||||||
|
|
@ -63,8 +69,12 @@ public sealed class MiningCut1Tests : IDisposable
|
||||||
var items = new ItemManager(cache, new ProvenanceLedger(), GameSettings(), _ => { });
|
var items = new ItemManager(cache, new ProvenanceLedger(), GameSettings(), _ => { });
|
||||||
var settings = Settings();
|
var settings = Settings();
|
||||||
|
|
||||||
var rootOrbit = new OrbitData { Distance = RootDistance, Phase = RootPhase };
|
var sunOrbitKey = cache.Upsert(new OrbitData()).Key;
|
||||||
|
var sunKey = cache.Upsert(new PlanetData { Name = "Sun", Orbit = new CultRecordRef<OrbitData>(sunOrbitKey), Mass = SunStrength }).Key;
|
||||||
|
|
||||||
|
var rootOrbit = new OrbitData { Parent = new CultRecordRef<OrbitData>(sunOrbitKey), Distance = RootDistance, Phase = RootPhase };
|
||||||
var rootKey = cache.Upsert(rootOrbit).Key;
|
var rootKey = cache.Upsert(rootOrbit).Key;
|
||||||
|
var planetKey = cache.Upsert(new PlanetData { Name = "Planet", Orbit = new CultRecordRef<OrbitData>(rootKey), Mass = PlanetStrength }).Key;
|
||||||
|
|
||||||
var beltOrbit = new OrbitData { Parent = new CultRecordRef<OrbitData>(rootKey), Distance = 900f, Phase = 0.42f };
|
var beltOrbit = new OrbitData { Parent = new CultRecordRef<OrbitData>(rootKey), Distance = 900f, Phase = 0.42f };
|
||||||
var beltOrbitKey = cache.Upsert(beltOrbit).Key;
|
var beltOrbitKey = cache.Upsert(beltOrbit).Key;
|
||||||
|
|
@ -86,8 +96,8 @@ public sealed class MiningCut1Tests : IDisposable
|
||||||
|
|
||||||
var pack = new ZonePack
|
var pack = new ZonePack
|
||||||
{
|
{
|
||||||
Orbits = { new CultRecordRef<OrbitData>(rootKey), new CultRecordRef<OrbitData>(beltOrbitKey) },
|
Orbits = { new CultRecordRef<OrbitData>(sunOrbitKey), new CultRecordRef<OrbitData>(rootKey), new CultRecordRef<OrbitData>(beltOrbitKey) },
|
||||||
Planets = { new CultRecordRef<BodyData>(beltKey) },
|
Planets = { new CultRecordRef<BodyData>(sunKey), new CultRecordRef<BodyData>(planetKey), new CultRecordRef<BodyData>(beltKey) },
|
||||||
Radius = 5000f,
|
Radius = 5000f,
|
||||||
Mass = 10000f,
|
Mass = 10000f,
|
||||||
Time = 0
|
Time = 0
|
||||||
|
|
@ -97,22 +107,22 @@ public sealed class MiningCut1Tests : IDisposable
|
||||||
return (zone, beltKey, asteroids, settings);
|
return (zone, beltKey, asteroids, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The formula of Zone.GetOrbitPosition (unchanged by this cut), reimplemented here so the expected value
|
// Zone.GetOrbitPosition's placement round a body of well strength `strength`, with the period spelled here from
|
||||||
// never calls production orbit code.
|
// the field law outside the core (a = c/d, so T = 2 pi d / sqrt(c)), so the expected value never calls
|
||||||
private static float2 OrbitPosition(double time, float distance, float phase, PlanetSettings settings, float2 fixedPosition = default)
|
// production orbit code.
|
||||||
|
private static float2 OrbitPosition(double time, float distance, float phase, float strength, float2 fixedPosition = default)
|
||||||
{
|
{
|
||||||
var period = settings.OrbitPeriod.Evaluate(distance);
|
var period = 2 * PI * distance / sqrt(strength);
|
||||||
if (period <= .01f) return fixedPosition;
|
|
||||||
var wrapped = (float) frac(time / period);
|
var wrapped = (float) frac(time / period);
|
||||||
return OrbitData.Evaluate(frac(wrapped + phase)) * distance + fixedPosition;
|
return OrbitData.Evaluate(frac(wrapped + phase)) * distance + fixedPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The formula of AsteroidBelt.Pose (Zone.cs:269-271 before this cut), reimplemented independently.
|
// The pose of an asteroid circling a planet of well strength `strength`, spelled independently of AsteroidBelt.Pose.
|
||||||
private static float4 ExpectedPose(double time, Asteroid asteroid, float2 parentPosition, PlanetSettings settings, float size)
|
private static float4 ExpectedPose(double time, Asteroid asteroid, float2 parentPosition, float strength, float size)
|
||||||
{
|
{
|
||||||
var rot = (float) (time * asteroid.RotationSpeed % (PI * 2));
|
var rot = (float) (time * asteroid.RotationSpeed % (PI * 2));
|
||||||
var pos = OrbitData.Evaluate((float) frac(time / settings.OrbitPeriod.Evaluate(asteroid.Distance) + asteroid.Phase))
|
var period = 2 * PI * asteroid.Distance / sqrt(strength);
|
||||||
* asteroid.Distance + parentPosition;
|
var pos = OrbitData.Evaluate((float) frac(time / period + asteroid.Phase)) * asteroid.Distance + parentPosition;
|
||||||
return float4(pos.x, pos.y, rot, size);
|
return float4(pos.x, pos.y, rot, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,11 +146,11 @@ public sealed class MiningCut1Tests : IDisposable
|
||||||
time += dt;
|
time += dt;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parentPosition = OrbitPosition(time, RootDistance, RootPhase, settings);
|
var parentPosition = OrbitPosition(time, RootDistance, RootPhase, SunStrength);
|
||||||
|
|
||||||
for (var i = 0; i < asteroids.Length; i++)
|
for (var i = 0; i < asteroids.Length; i++)
|
||||||
{
|
{
|
||||||
var expected = ExpectedPose(time, asteroids[i], parentPosition, settings, settings.AsteroidSize.Evaluate(asteroids[i].Size));
|
var expected = ExpectedPose(time, asteroids[i], parentPosition, PlanetStrength, settings.AsteroidSize.Evaluate(asteroids[i].Size));
|
||||||
var actual = zone.ChunkPose(beltKey, i);
|
var actual = zone.ChunkPose(beltKey, i);
|
||||||
AssertClose(expected.x, actual.x, 0.01f, $"asteroid {i} x");
|
AssertClose(expected.x, actual.x, 0.01f, $"asteroid {i} x");
|
||||||
AssertClose(expected.y, actual.y, 0.01f, $"asteroid {i} y");
|
AssertClose(expected.y, actual.y, 0.01f, $"asteroid {i} y");
|
||||||
|
|
@ -157,13 +167,13 @@ public sealed class MiningCut1Tests : IDisposable
|
||||||
{
|
{
|
||||||
var (zone, beltKey, asteroids, settings) = BuildFixture();
|
var (zone, beltKey, asteroids, settings) = BuildFixture();
|
||||||
zone.Update(40f);
|
zone.Update(40f);
|
||||||
var parentPosition = OrbitPosition(40.0, RootDistance, RootPhase, settings);
|
var parentPosition = OrbitPosition(40.0, RootDistance, RootPhase, SunStrength);
|
||||||
|
|
||||||
var wrapped = 0;
|
var wrapped = 0;
|
||||||
for (var i = 0; i < asteroids.Length; i++)
|
for (var i = 0; i < asteroids.Length; i++)
|
||||||
{
|
{
|
||||||
if (40.0 * asteroids[i].RotationSpeed > PI * 2) wrapped++;
|
if (40.0 * asteroids[i].RotationSpeed > PI * 2) wrapped++;
|
||||||
var expected = ExpectedPose(40.0, asteroids[i], parentPosition, settings, settings.AsteroidSize.Evaluate(asteroids[i].Size));
|
var expected = ExpectedPose(40.0, asteroids[i], parentPosition, PlanetStrength, settings.AsteroidSize.Evaluate(asteroids[i].Size));
|
||||||
var actual = zone.ChunkPose(beltKey, i);
|
var actual = zone.ChunkPose(beltKey, i);
|
||||||
AssertClose(expected.z, actual.z, 0.001f, $"asteroid {i} rotation");
|
AssertClose(expected.z, actual.z, 0.001f, $"asteroid {i} rotation");
|
||||||
Assert.InRange(actual.z, 0f, PI * 2);
|
Assert.InRange(actual.z, 0f, PI * 2);
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ public sealed class MiningCut2Tests : IDisposable
|
||||||
// curve" is exercising real curvature, not a constant that any wrong formula would also satisfy.
|
// curve" is exercising real curvature, not a constant that any wrong formula would also satisfy.
|
||||||
private static PlanetSettings Settings() => new PlanetSettings
|
private static PlanetSettings Settings() => new PlanetSettings
|
||||||
{
|
{
|
||||||
OrbitPeriod = new ExponentialCurve { Multiplier = 0.6f, Exponent = 1.3f, Constant = 40f },
|
WellStrength = new ExponentialCurve { Constant = 25f },
|
||||||
|
RocheLimit = new ExponentialCurve { Constant = 1f },
|
||||||
AsteroidSize = new ExponentialLerp { Minimum = 2f, Maximum = 9f, Exponent = 1.7f },
|
AsteroidSize = new ExponentialLerp { Minimum = 2f, Maximum = 9f, Exponent = 1.7f },
|
||||||
AsteroidHitpoints = new ExponentialLerp { Minimum = 20f, Maximum = 220f, Exponent = 1.6f },
|
AsteroidHitpoints = new ExponentialLerp { Minimum = 20f, Maximum = 220f, Exponent = 1.6f },
|
||||||
AsteroidRespawnTime = new ExponentialLerp { Minimum = 8f, Maximum = 96f, Exponent = 1.2f },
|
AsteroidRespawnTime = new ExponentialLerp { Minimum = 8f, Maximum = 96f, Exponent = 1.2f },
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,12 @@ public sealed partial class MiningCut3Tests : IDisposable
|
||||||
FiringArc = 90f
|
FiringArc = 90f
|
||||||
};
|
};
|
||||||
|
|
||||||
// Flat terrain (no zone depth, no gravity wells), one sun whose light reaches 1000 units, and the chunk
|
// Every sun carries a well (the belts circle in it, c = 9), one sun whose light reaches 1000 units, and the chunk
|
||||||
// curves MiningCut2Tests uses, so sizes and wear vary over the fixture's range.
|
// curves MiningCut2Tests uses, so sizes and wear vary over the fixture's range.
|
||||||
private static PlanetSettings PlanetSettings(float lightRadius = 1000f) => new PlanetSettings
|
private static PlanetSettings PlanetSettings(float lightRadius = 1000f) => new PlanetSettings
|
||||||
{
|
{
|
||||||
OrbitPeriod = new ExponentialCurve { Multiplier = 0.6f, Exponent = 1.3f, Constant = 40f },
|
WellStrength = new ExponentialCurve { Constant = 9f },
|
||||||
|
RocheLimit = new ExponentialCurve { Constant = 1f },
|
||||||
AsteroidSize = new ExponentialLerp { Minimum = 2f, Maximum = 9f, Exponent = 1.7f },
|
AsteroidSize = new ExponentialLerp { Minimum = 2f, Maximum = 9f, Exponent = 1.7f },
|
||||||
AsteroidHitpoints = new ExponentialLerp { Minimum = 20f, Maximum = 220f, Exponent = 1.6f },
|
AsteroidHitpoints = new ExponentialLerp { Minimum = 20f, Maximum = 220f, Exponent = 1.6f },
|
||||||
AsteroidRespawnTime = new ExponentialLerp { Minimum = 8f, Maximum = 96f, Exponent = 1.2f },
|
AsteroidRespawnTime = new ExponentialLerp { Minimum = 8f, Maximum = 96f, Exponent = 1.2f },
|
||||||
|
|
|
||||||
|
|
@ -458,7 +458,8 @@ public sealed partial class MiningCut3Tests
|
||||||
var spread = Enumerable.Range(0, 700).Select(_ => Rock(50f + (float) rng.NextDouble() * 5950f, (float) rng.NextDouble(), (float) rng.NextDouble())).ToArray();
|
var spread = Enumerable.Range(0, 700).Select(_ => Rock(50f + (float) rng.NextDouble() * 5950f, (float) rng.NextDouble(), (float) rng.NextDouble())).ToArray();
|
||||||
var s = BuildSceneAt(centre, 20000f, 777.25, null, new[] { kind }, Belt(kind, spread), Belt(kind, Annulus(rng, 3000, 999f, 1001f)));
|
var s = BuildSceneAt(centre, 20000f, 777.25, null, new[] { kind }, Belt(kind, spread), Belt(kind, Annulus(rng, 3000, 999f, 1001f)));
|
||||||
var game = PlanetSettings(20000f);
|
var game = PlanetSettings(20000f);
|
||||||
game.OrbitPeriod = new ExponentialCurve { Multiplier = 1f, Exponent = 1f, Constant = 0f };
|
// The game's periods: T = 2 pi d / sqrt(c) with c = 4 pi^2 is T = d.
|
||||||
|
game.WellStrength = new ExponentialCurve { Constant = 4f * PI * PI };
|
||||||
var searches = 0;
|
var searches = 0;
|
||||||
foreach (var zone in new[] { s.Zone, new Zone(s.Items, game, s.Pack, new GalaxyZone { Name = "Game periods", Owner = null }, null) })
|
foreach (var zone in new[] { s.Zone, new Zone(s.Items, game, s.Pack, new GalaxyZone { Name = "Game periods", Owner = null }, null) })
|
||||||
foreach (var step in new[] { 0f, 13f, 1e4f, 3.3e7f })
|
foreach (var step in new[] { 0f, 13f, 1e4f, 3.3e7f })
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ public sealed class PotentialOwnerTests : IDisposable
|
||||||
WellStrength = new ExponentialCurve { Exponent = .2f, Multiplier = 45.08f },
|
WellStrength = new ExponentialCurve { Exponent = .2f, Multiplier = 45.08f },
|
||||||
RocheLimit = new ExponentialCurve { Exponent = .25f, Multiplier = 7.32f },
|
RocheLimit = new ExponentialCurve { Exponent = .25f, Multiplier = 7.32f },
|
||||||
BodyRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 2f },
|
BodyRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 2f },
|
||||||
LightRadius = new ExponentialCurve(), OrbitPeriod = new ExponentialCurve(),
|
LightRadius = new ExponentialCurve(),
|
||||||
HeightExaggeration = exaggeration
|
HeightExaggeration = exaggeration
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ public sealed class PotentialOwnerTests : IDisposable
|
||||||
var planets = new PlanetSettings
|
var planets = new PlanetSettings
|
||||||
{
|
{
|
||||||
BodyRadius = new ExponentialCurve(),
|
BodyRadius = new ExponentialCurve(),
|
||||||
LightRadius = new ExponentialCurve(), OrbitPeriod = new ExponentialCurve()
|
LightRadius = new ExponentialCurve()
|
||||||
};
|
};
|
||||||
var zone = ZoneOf(planets, (100f, float2.zero), (5000f, new float2(200f, 0f)));
|
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) })
|
foreach (var p in new[] { float2.zero, float2(200f, 0f), float2(37f, -911f), float2(5000f, 5000f) })
|
||||||
|
|
@ -138,7 +138,7 @@ public sealed class PotentialOwnerTests : IDisposable
|
||||||
WellStrength = new ExponentialCurve { Constant = 100f },
|
WellStrength = new ExponentialCurve { Constant = 100f },
|
||||||
RocheLimit = new ExponentialCurve { Constant = roche },
|
RocheLimit = new ExponentialCurve { Constant = roche },
|
||||||
BodyRadius = new ExponentialCurve { Constant = 10f },
|
BodyRadius = new ExponentialCurve { Constant = 10f },
|
||||||
LightRadius = new ExponentialCurve(), OrbitPeriod = new ExponentialCurve()
|
LightRadius = new ExponentialCurve()
|
||||||
};
|
};
|
||||||
Assert.Throws<InvalidOperationException>(() => ZoneOf(Constants(0f), (100f, float2.zero)));
|
Assert.Throws<InvalidOperationException>(() => ZoneOf(Constants(0f), (100f, float2.zero)));
|
||||||
Assert.Throws<InvalidOperationException>(() => ZoneOf(Constants(-5f), (100f, float2.zero)));
|
Assert.Throws<InvalidOperationException>(() => ZoneOf(Constants(-5f), (100f, float2.zero)));
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ internal static class WellFixture
|
||||||
public static void Dig(CultCache cache, ZonePack pack, PlanetSettings settings, float strength, float coreRadius, float2 at = default)
|
public static void Dig(CultCache cache, ZonePack pack, PlanetSettings settings, float strength, float coreRadius, float2 at = default)
|
||||||
{
|
{
|
||||||
settings.BodyRadius = new ExponentialCurve();
|
settings.BodyRadius = new ExponentialCurve();
|
||||||
settings.OrbitPeriod = new ExponentialCurve();
|
|
||||||
settings.WellStrength = new ExponentialCurve { Constant = strength };
|
settings.WellStrength = new ExponentialCurve { Constant = strength };
|
||||||
settings.RocheLimit = new ExponentialCurve { Constant = coreRadius };
|
settings.RocheLimit = new ExponentialCurve { Constant = coreRadius };
|
||||||
var orbit = cache.Upsert(new OrbitData { FixedPosition = at }).Key;
|
var orbit = cache.Upsert(new OrbitData { FixedPosition = at }).Key;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public sealed class ZonePlacedAtBuildTests : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly float2 RootAt = new float2(300f, -200f);
|
private static readonly float2 RootAt = new float2(300f, -200f);
|
||||||
private const float Distance = 400f;
|
private const float Distance = 400f;
|
||||||
private const float Period = 40f;
|
private const float SunStrength = 100f; // the root's body: the planet circles in its field
|
||||||
|
|
||||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-zoneplaced-" + Guid.NewGuid().ToString("N"));
|
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-zoneplaced-" + Guid.NewGuid().ToString("N"));
|
||||||
private readonly List<CultCache> _caches = new List<CultCache>();
|
private readonly List<CultCache> _caches = new List<CultCache>();
|
||||||
|
|
@ -38,13 +38,13 @@ public sealed class ZonePlacedAtBuildTests : IDisposable
|
||||||
|
|
||||||
private static ExponentialCurve Flat(float v) => new ExponentialCurve { Constant = v };
|
private static ExponentialCurve Flat(float v) => new ExponentialCurve { Constant = v };
|
||||||
|
|
||||||
// One planet with a well (c 100, Roche limit and core 20 u), a fixed root orbit at RootAt and the
|
// A sun on the fixed root orbit at RootAt, a planet orbiting it at Distance from `phase`, and a light moon body on
|
||||||
// planet orbiting it at Distance from `phase`. Every orbit period is Period seconds.
|
// the moon orbit so the moon's moon has a field to circle in. A body's well is 100 per unit of mass; every core
|
||||||
|
// (Roche limit) is 20 u.
|
||||||
private static PlanetSettings Settings() => new PlanetSettings
|
private static PlanetSettings Settings() => new PlanetSettings
|
||||||
{
|
{
|
||||||
OrbitPeriod = Flat(Period),
|
|
||||||
BodyRadius = Flat(20f),
|
BodyRadius = Flat(20f),
|
||||||
WellStrength = Flat(100f),
|
WellStrength = new ExponentialCurve { Multiplier = 100f, Exponent = 1f },
|
||||||
RocheLimit = Flat(20f),
|
RocheLimit = Flat(20f),
|
||||||
LightRadius = Flat(0f)
|
LightRadius = Flat(0f)
|
||||||
};
|
};
|
||||||
|
|
@ -86,10 +86,12 @@ public sealed class ZonePlacedAtBuildTests : IDisposable
|
||||||
{
|
{
|
||||||
Parent = new CultRecordRef<OrbitData>(moonOrbit), Distance = 40f
|
Parent = new CultRecordRef<OrbitData>(moonOrbit), Distance = 40f
|
||||||
}).Key;
|
}).Key;
|
||||||
|
var sun = cache.Upsert(new PlanetData { Orbit = new CultRecordRef<OrbitData>(rootOrbit), Mass = 1f }).Key;
|
||||||
var planet = cache.Upsert(new PlanetData { Orbit = new CultRecordRef<OrbitData>(planetOrbit), Mass = 1f }).Key;
|
var planet = cache.Upsert(new PlanetData { Orbit = new CultRecordRef<OrbitData>(planetOrbit), Mass = 1f }).Key;
|
||||||
|
var moon = cache.Upsert(new PlanetData { Orbit = new CultRecordRef<OrbitData>(moonOrbit), Mass = .01f }).Key;
|
||||||
var pack = new ZonePack
|
var pack = new ZonePack
|
||||||
{
|
{
|
||||||
Planets = { new CultRecordRef<BodyData>(planet) },
|
Planets = { new CultRecordRef<BodyData>(sun), new CultRecordRef<BodyData>(planet), new CultRecordRef<BodyData>(moon) },
|
||||||
Radius = 5000f,
|
Radius = 5000f,
|
||||||
Mass = 10000f,
|
Mass = 10000f,
|
||||||
Time = time
|
Time = time
|
||||||
|
|
@ -154,7 +156,7 @@ public sealed class ZonePlacedAtBuildTests : IDisposable
|
||||||
public void TheFirstTickVelocityIsTheOrbitsSpeed(bool moonFirst)
|
public void TheFirstTickVelocityIsTheOrbitsSpeed(bool moonFirst)
|
||||||
{
|
{
|
||||||
var s = Build(.35f, 0.0, moonFirst);
|
var s = Build(.35f, 0.0, moonFirst);
|
||||||
var expected = 2f * PI * Distance / Period;
|
var expected = sqrt(SunStrength); // outside the core T = 2 pi d / sqrt(c): the speed is sqrt(c), whatever d
|
||||||
foreach (var dt in new[] { 1f / 60f, 1f / 30f, 1f / 60f })
|
foreach (var dt in new[] { 1f / 60f, 1f / 30f, 1f / 60f })
|
||||||
{
|
{
|
||||||
s.Zone.Update(dt);
|
s.Zone.Update(dt);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue