WellLawTests: the term's slope and band, the family laws, reach and support; ZoneWorlds shared by the tree tests
Hand-built zones move from OrbitsFromFieldTests into ZoneWorlds so WellLawTests reuses them. Not yet run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1068eca331
commit
9d5620ddac
3 changed files with 339 additions and 59 deletions
|
|
@ -16,65 +16,12 @@ using float3 = CultMath.float3;
|
|||
// spelled here as T = 2 pi sqrt(d / a), a composed from GravityField.Pull and c and eps read from the zone's own bodies.
|
||||
public sealed class OrbitsFromFieldTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-orbitsfield-" + Guid.NewGuid().ToString("N"));
|
||||
private readonly List<CultCache> _caches = new List<CultCache>();
|
||||
private readonly ZoneWorlds _worlds = new ZoneWorlds();
|
||||
|
||||
public OrbitsFromFieldTests() => Directory.CreateDirectory(_root);
|
||||
public void Dispose() => _worlds.Dispose();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var cache in _caches) cache.Dispose();
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private static GameplaySettings GameSettings() => new GameplaySettings
|
||||
{
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp()
|
||||
};
|
||||
|
||||
private static PlanetSettings Curves() => new PlanetSettings
|
||||
{
|
||||
WellStrength = new ExponentialCurve { Exponent = .2f, Multiplier = 45.08f },
|
||||
RocheLimit = new ExponentialCurve { Exponent = .25f, Multiplier = 7.32f },
|
||||
BodyRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 3f },
|
||||
LightRadius = new ExponentialCurve()
|
||||
};
|
||||
|
||||
private sealed class World
|
||||
{
|
||||
public CultCache Cache;
|
||||
public ItemManager Items;
|
||||
public ZonePack Pack = new ZonePack { Radius = 20000f, Mass = 10000f };
|
||||
|
||||
public CultRecordKey Orbit(CultRecordKey parent, float distance, float phase = 0f)
|
||||
{
|
||||
var data = new OrbitData { Distance = distance, Phase = phase };
|
||||
if (parent.IsSet()) data.Parent = new CultRecordRef<OrbitData>(parent);
|
||||
var key = Cache.Upsert(data).Key;
|
||||
Pack.Orbits.Add(new CultRecordRef<OrbitData>(key));
|
||||
return key;
|
||||
}
|
||||
|
||||
public CultRecordKey Body(CultRecordKey orbit, float mass)
|
||||
{
|
||||
var key = Cache.Upsert(new PlanetData { Name = "Body", Orbit = new CultRecordRef<OrbitData>(orbit), Mass = mass }).Key;
|
||||
Pack.Planets.Add(new CultRecordRef<BodyData>(key));
|
||||
return key;
|
||||
}
|
||||
|
||||
public Zone Build() => new Zone(Items, Curves(), Pack, new GalaxyZone { Name = "OrbitsFromField", Owner = null }, null);
|
||||
}
|
||||
|
||||
private World NewWorld()
|
||||
{
|
||||
var dir = Path.Combine(_root, $"world{_caches.Count}");
|
||||
Directory.CreateDirectory(dir);
|
||||
var cache = AetheriaStores.Open(Path.Combine(dir, "Aetheria.cc"), Path.Combine(dir, "run.cc"), catalogWritable: true);
|
||||
_caches.Add(cache);
|
||||
return new World { Cache = cache, Items = new ItemManager(cache, new ProvenanceLedger(), GameSettings(), _ => { }) };
|
||||
}
|
||||
private ZoneWorlds.World NewWorld() => _worlds.New();
|
||||
private static PlanetSettings Curves() => ZoneWorlds.Curves();
|
||||
|
||||
private static float C(Zone zone, CultRecordKey body) => zone.PlanetInstances[body].WellStrength;
|
||||
private static float Core(Zone zone, CultRecordKey body) => zone.PlanetInstances[body].RocheLimit;
|
||||
|
|
@ -330,12 +277,12 @@ public sealed class OrbitsFromFieldTests : IDisposable
|
|||
var binaries = 0;
|
||||
for (uint seed = 1; seed <= 12; seed++)
|
||||
{
|
||||
var dir = Path.Combine(_root, $"galaxy{seed}");
|
||||
var dir = Path.Combine(_worlds.Root, $"galaxy{seed}");
|
||||
Directory.CreateDirectory(dir);
|
||||
var catalog = Path.Combine(dir, "Aetheria.cc");
|
||||
File.Copy(TestCatalog.Repo, catalog);
|
||||
var cache = new CultCache(TestCatalog.Registry());
|
||||
_caches.Add(cache);
|
||||
_worlds.Caches.Add(cache);
|
||||
cache.AddBackingStore(new GameCult.Caching.MessagePack.SingleFileMessagePackBackingStore(catalog), AetheriaStores.CatalogTypes);
|
||||
cache.AddBackingStore(new GameCult.Caching.MessagePack.SingleFileMessagePackBackingStore(Path.Combine(dir, "run.cc")), AetheriaStores.RunTypes);
|
||||
var authored = AuthoredSettings.Load(RunStartTests.FindRepoRoot());
|
||||
|
|
|
|||
258
tests/Aetheria.Shared.Tests/WellLawTests.cs
Normal file
258
tests/Aetheria.Shared.Tests/WellLawTests.cs
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
|
||||
// aetheria-gravity, cut well-law: the conserving compact law. GravityField is the law of one band term; WellTree turns a zone's
|
||||
// orbit tree into terms and walks them. The oracles here are written out from the law (the order-4 smoothstep, Simpson
|
||||
// integration of the pull in double, merging a family into one body), never by calling the code under test twice.
|
||||
public sealed class WellLawTests : IDisposable
|
||||
{
|
||||
private readonly ZoneWorlds _worlds = new ZoneWorlds();
|
||||
|
||||
public void Dispose() => _worlds.Dispose();
|
||||
|
||||
// The Plummer pull of a term without a band: r / (r^2 + eps^2).
|
||||
private static double Plummer(double eps, double r) => r / (r * r + eps * eps);
|
||||
|
||||
// The order-4 smoothstep, spelled: S(t) = 35 t^4 - 84 t^5 + 70 t^6 - 20 t^7.
|
||||
private static double S(double t) => t * t * t * t * (35 - 84 * t + 70 * t * t - 20 * t * t * t);
|
||||
|
||||
private static double Simpson(Func<double, double> f, double from, double to, int n = 4000)
|
||||
{
|
||||
var h = (to - from) / n;
|
||||
var sum = f(from) + f(to);
|
||||
for (var i = 1; i < n; i++) sum += f(from + i * h) * (i % 2 == 1 ? 4 : 2);
|
||||
return sum * h / 3;
|
||||
}
|
||||
|
||||
public static readonly object[][] Terms =
|
||||
{
|
||||
new object[] { 79.8f, 1000f, 1500f },
|
||||
new object[] { 17f, 40f, 60f },
|
||||
new object[] { 79.8f, 0f, 1525f },
|
||||
new object[] { 1658f, 1000f, 1500f }, // tau 3: the series branch
|
||||
new object[] { 59f, 11f, 22f }, // tau 10
|
||||
new object[] { 79.8f, float.PositiveInfinity, float.PositiveInfinity }
|
||||
};
|
||||
|
||||
// The pull is the slope of the potential: the potential's drop between two radii is the integral of the pull between
|
||||
// them, by Simpson in double, for terms on both branches, inside the band, across its edges and beyond it.
|
||||
[Theory]
|
||||
[MemberData(nameof(Terms))]
|
||||
public void TheTermIsItsPotentialsSlope(float eps, float inner, float outer)
|
||||
{
|
||||
var span = float.IsPositiveInfinity(outer) ? 3000f : outer;
|
||||
foreach (var (from, to) in new[] { (.2f, .55f), (.4f, .99f), (.9f, 1.3f), (.05f, 1.0f) })
|
||||
{
|
||||
float r0 = from * span, r1 = Math.Min(to * span, float.IsPositiveInfinity(outer) ? 6000f : outer * 1.1f);
|
||||
var drop = GravityField.Potential(eps, inner, outer, r1) - GravityField.Potential(eps, inner, outer, r0);
|
||||
var integral = Simpson(r => GravityField.Pull(eps, inner, outer, (float) r), r0, r1);
|
||||
Assert.InRange(drop, integral - 3e-5 - 1e-4 * Math.Abs(integral), integral + 3e-5 + 1e-4 * Math.Abs(integral));
|
||||
}
|
||||
if (!float.IsPositiveInfinity(outer))
|
||||
{
|
||||
Assert.Equal(0f, GravityField.Potential(eps, inner, outer, outer));
|
||||
Assert.Equal(0f, GravityField.Potential(eps, inner, outer, outer * 1.5f));
|
||||
Assert.Equal(0f, GravityField.Pull(eps, inner, outer, outer * 1.5f));
|
||||
}
|
||||
}
|
||||
|
||||
// The pull is the Plummer pull faded by 1 - S(t) across the band, S the order-4 smoothstep: a smoothstep of order 2 has a
|
||||
// different profile, and the profile's first three derivatives vanish at both edges (the deficit goes as t^4).
|
||||
[Fact]
|
||||
public void ABandEdgeIsSmooth()
|
||||
{
|
||||
const float eps = 79.8f, a = 1000f, b = 1500f;
|
||||
foreach (var t in new[] { 0f, .02f, .05f, .1f, .3f, .5f, .7f, .9f, .98f, 1f })
|
||||
{
|
||||
var r = (float) Math.Sqrt(a * a + t * (b * b - a * a));
|
||||
var pull = GravityField.Pull(eps, a, b, r);
|
||||
var expected = Plummer(eps, r) * (1 - S(t));
|
||||
Assert.InRange(pull, expected - 1e-5 * Plummer(eps, r), expected + 1e-5 * Plummer(eps, r));
|
||||
}
|
||||
Assert.Equal((double) GravityField.Pull(eps, a, b, a), Plummer(eps, a), 6);
|
||||
Assert.Equal(0f, GravityField.Pull(eps, a, b, b));
|
||||
var deficit = new Func<float, double>(t => 1 - GravityField.Pull(eps, a, b, (float) Math.Sqrt(a * a + t * (b * b - a * a))) / Plummer(eps, Math.Sqrt(a * a + t * (b * b - a * a))));
|
||||
Assert.InRange(deficit(.1f) / deficit(.05f), 13.0, 15.0); // doubling t multiplies the deficit by about 14 (t^4), not by 4
|
||||
}
|
||||
|
||||
// tau = (a^2 + eps^2) / (b^2 - a^2): the closed form holds to tau 1 and the series beyond it, and the two meet; in single
|
||||
// precision the tau 10 potential stays within 1e-5 of the double-precision integral of the pull.
|
||||
[Fact]
|
||||
public void TheSeriesBranchMeetsTheClosedForm()
|
||||
{
|
||||
// tau 1 +- 1e-6: eps^2 = b^2 - 2 a^2 gives tau 1 exactly.
|
||||
const float a = 0.8f, b = 1.28062487f;
|
||||
var below = (float) Math.Sqrt(b * b - 2 * a * a) * (1 - 1e-6f);
|
||||
var above = (float) Math.Sqrt(b * b - 2 * a * a) * (1 + 1e-6f);
|
||||
foreach (var r in new[] { 0.5f, 0.85f, 1.0f, 1.2f })
|
||||
Assert.InRange(GravityField.Potential(above, a, b, r) - GravityField.Potential(below, a, b, r), -3e-6f, 3e-6f);
|
||||
|
||||
const float eps = 59f, inner = 11f, outer = 22f;
|
||||
foreach (var r in new[] { 12f, 15f, 18f, 21f })
|
||||
{
|
||||
var oracle = -Simpson(x => GravityField.Pull(eps, inner, outer, (float) x), r, outer);
|
||||
Assert.InRange(GravityField.Potential(eps, inner, outer, r), oracle - 1e-5, oracle + 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
private static float Plummer(PlanetSettings curves, float mass, float d) =>
|
||||
curves.WellStrength.Evaluate(mass) * d / (d * d + curves.RocheLimit.Evaluate(mass) * curves.RocheLimit.Evaluate(mass));
|
||||
|
||||
// A sun, a planet at 3000 and a moon at 150 round the planet, the moon's mass as given.
|
||||
private Zone SunPlanetMoon(float moonMass, out CultRecordKey planetOrbit, out CultRecordKey moonOrbit, float planetMass = 100f)
|
||||
{
|
||||
var w = _worlds.New();
|
||||
var sunOrbit = w.Orbit(default, 0f);
|
||||
w.Body(sunOrbit, 5000f);
|
||||
planetOrbit = w.Orbit(sunOrbit, 3000f);
|
||||
w.Body(planetOrbit, planetMass);
|
||||
moonOrbit = w.Orbit(planetOrbit, 150f);
|
||||
if (moonMass > 0f) w.Body(moonOrbit, moonMass);
|
||||
return w.Build();
|
||||
}
|
||||
|
||||
// Inside its outermost child a node pulls exactly as its body alone: the family terms cancel there, so a child's rail
|
||||
// never feels its own mass through its parent.
|
||||
[Fact]
|
||||
public void InsideAFamilyItsParentPullsAsItsBodyAlone()
|
||||
{
|
||||
var curves = ZoneWorlds.Curves();
|
||||
foreach (var moonMass in new[] { 1f, 2f })
|
||||
{
|
||||
var zone = SunPlanetMoon(moonMass, out var planetOrbit, out var moonOrbit);
|
||||
foreach (var d in new[] { 20f, 60f, 150f })
|
||||
{
|
||||
var expected = Plummer(curves, 100f, d);
|
||||
Assert.InRange(zone.Wells.PullAround(planetOrbit, d, moonOrbit), expected * (1 - 1e-5f), expected * (1 + 1e-5f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merging a family into one body of its mass in the same slot changes nothing beyond the family's support, and does
|
||||
// inside it.
|
||||
[Fact]
|
||||
public void AFamilyMergedIntoOneBodyIsTheSameFieldOutsideIt()
|
||||
{
|
||||
var family = SunPlanetMoon(1f, out var planetOrbit, out _);
|
||||
var merged = SunPlanetMoon(0f, out _, out _, planetMass: 101f);
|
||||
var support = family.Wells.Support(planetOrbit);
|
||||
Assert.True(support > 150f, "fixture: the support clears the moon");
|
||||
var planetAt = family.Orbits[planetOrbit].Position;
|
||||
foreach (var (distance, angle) in new[] { (1.05f, .3f), (1.4f, 2.2f), (3f, 4.0f), (9f, 1.0f), (20f, 5.5f) })
|
||||
{
|
||||
var p = planetAt + float2(support * distance * cos(angle), support * distance * sin(angle));
|
||||
var f = family.GetForce(p);
|
||||
var m = merged.GetForce(p);
|
||||
Assert.InRange(length(f - m), 0f, 1e-5f * length(f) + 1e-9f);
|
||||
Assert.InRange(abs(family.GetHeight(p) - merged.GetHeight(p)), 0f, 1e-5f * (1f + abs(family.GetHeight(p))));
|
||||
}
|
||||
var inside = planetAt + float2(100f, 0f);
|
||||
Assert.True(length(family.GetForce(inside) - merged.GetForce(inside)) > 1e-3f * length(merged.GetForce(inside)), "inside the support the moon is a body of its own");
|
||||
}
|
||||
|
||||
// The root has no reach: far from everything the zone is one body of its total mass, however the generator split it.
|
||||
[Fact]
|
||||
public void TheZonesFarFieldIsItsTotalMass()
|
||||
{
|
||||
var curves = ZoneWorlds.Curves();
|
||||
var one = SunPlanetMoon(1f, out _, out _);
|
||||
var w = _worlds.New();
|
||||
var sunOrbit = w.Orbit(default, 0f);
|
||||
w.Body(sunOrbit, 5000f);
|
||||
for (var i = 0; i < 5; i++) w.Body(w.Orbit(sunOrbit, 800f + 450f * i, i / 5f), 20.2f);
|
||||
var five = w.Build();
|
||||
foreach (var zone in new[] { one, five })
|
||||
{
|
||||
var far = 10f * zone.Pack.Radius;
|
||||
foreach (var angle in new[] { 0f, 1.7f, 4.1f })
|
||||
Near(curves.WellStrength.Evaluate(5101f), length(zone.GetForce(float2(far * cos(angle), far * sin(angle)))) * far, 1e-3f);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Near(float expected, float actual, float relative) =>
|
||||
Assert.InRange(actual, expected * (1 - relative), expected * (1 + relative));
|
||||
|
||||
// A node's reach is half its distance, or one and a half times its support if that is more; the root's is infinite.
|
||||
[Fact]
|
||||
public void AReachIsHalfTheDistanceOrOneAndAHalfTheSupport()
|
||||
{
|
||||
var w = _worlds.New();
|
||||
var sunOrbit = w.Orbit(default, 0f);
|
||||
w.Body(sunOrbit, 5000f);
|
||||
var wide = w.Orbit(sunOrbit, 3000f);
|
||||
w.Body(wide, 100f);
|
||||
var tight = w.Orbit(sunOrbit, 100f);
|
||||
w.Body(tight, 100f);
|
||||
w.Body(w.Orbit(wide, 150f), 1f);
|
||||
w.Body(w.Orbit(tight, 150f), 1f);
|
||||
var zone = w.Build();
|
||||
|
||||
Assert.True(float.IsPositiveInfinity(zone.Wells.Reach(sunOrbit)));
|
||||
Assert.Equal(1500f, zone.Wells.Reach(wide), 3);
|
||||
Assert.Equal(225f, zone.Wells.Support(wide), 3);
|
||||
Assert.Equal(337.5f, zone.Wells.Reach(tight), 3);
|
||||
Assert.Equal(225f, zone.Wells.Support(tight), 3);
|
||||
}
|
||||
|
||||
// A lone body on a bodyless node is pulled by nothing of its own: the node's terms cancel where the ring is.
|
||||
[Fact]
|
||||
public void ALoneBodyOnABodylessNodeFeelsNoPullOfItsOwn()
|
||||
{
|
||||
var w = _worlds.New();
|
||||
var sunOrbit = w.Orbit(default, 0f);
|
||||
w.Body(sunOrbit, 5000f);
|
||||
var node = w.Orbit(sunOrbit, 3000f);
|
||||
var alone = w.Orbit(node, 200f);
|
||||
w.Body(alone, 100f);
|
||||
var zone = w.Build();
|
||||
|
||||
Assert.Equal(0f, zone.Wells.PullAround(node, 200f, alone));
|
||||
Assert.True(float.IsPositiveInfinity(zone.Orbits[alone].Period));
|
||||
}
|
||||
|
||||
// The walk skips a subtree outside its reach, which is exact: at points all over a tree with families, a ring and a belt of
|
||||
// moons, the force and potential of the walk equal the flat sum over every term to float rounding of the terms.
|
||||
[Fact]
|
||||
public void TheWalkIsTheSum()
|
||||
{
|
||||
var w = _worlds.New();
|
||||
var sunOrbit = w.Orbit(default, 0f);
|
||||
w.Body(sunOrbit, 5000f);
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var planet = w.Orbit(sunOrbit, 700f + 800f * i, frac(i * .3819f));
|
||||
w.Body(planet, 40f + 30f * i);
|
||||
for (var j = 0; j < 3; j++) w.Body(w.Orbit(planet, 90f + 70f * j, frac(j * .6180339887f)), 1f + j);
|
||||
}
|
||||
var node = w.Orbit(sunOrbit, 5200f, .13f);
|
||||
w.Body(w.Orbit(node, 300f, 0f), 80f);
|
||||
w.Body(w.Orbit(node, 300f, .5f), 60f);
|
||||
var zone = w.Build();
|
||||
|
||||
var rng = new System.Random(7);
|
||||
for (var n = 0; n < 200; n++)
|
||||
{
|
||||
var p = float2((float) (rng.NextDouble() * 12000 - 6000), (float) (rng.NextDouble() * 12000 - 6000));
|
||||
var force = float2.zero;
|
||||
var potential = 0f;
|
||||
var scale = 0f;
|
||||
foreach (var term in zone.Wells.Terms)
|
||||
{
|
||||
var offset = zone.Orbits[term.Node].Position - p;
|
||||
var r = length(offset);
|
||||
var pull = term.Strength * GravityField.Pull(term.Softening, term.Inner, term.Outer, r);
|
||||
if (r > 0f) force += offset / r * pull;
|
||||
var level = term.Strength * GravityField.Potential(term.Softening, term.Inner, term.Outer, r) - term.Reference;
|
||||
potential += level;
|
||||
scale += abs(pull) + abs(level);
|
||||
}
|
||||
Assert.InRange(length(zone.GetForce(p) - force), 0f, 2e-6f * scale + 1e-9f);
|
||||
Assert.InRange(abs(zone.GetHeight(p) * zone.Settings.HeightExaggeration - potential), 0f, 2e-6f * scale + 1e-9f);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
tests/Aetheria.Shared.Tests/ZoneWorlds.cs
Normal file
75
tests/Aetheria.Shared.Tests/ZoneWorlds.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
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;
|
||||
|
||||
// The hand-built zones of the tree tests: packs of orbits and bodies read by the production Zone constructor with the shipped
|
||||
// well curves (c = 45.08 m^.2, eps = 7.32 m^.25), each in its own store under one temporary root.
|
||||
internal sealed class ZoneWorlds : IDisposable
|
||||
{
|
||||
public readonly string Root = Path.Combine(Path.GetTempPath(), "aetheria-orbitsfield-" + Guid.NewGuid().ToString("N"));
|
||||
public readonly List<CultCache> Caches = new List<CultCache>();
|
||||
|
||||
public ZoneWorlds() => Directory.CreateDirectory(Root);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var cache in Caches) cache.Dispose();
|
||||
Directory.Delete(Root, true);
|
||||
}
|
||||
|
||||
private static GameplaySettings GameSettings() => new GameplaySettings
|
||||
{
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp()
|
||||
};
|
||||
|
||||
public static PlanetSettings Curves() => new PlanetSettings
|
||||
{
|
||||
WellStrength = new ExponentialCurve { Exponent = .2f, Multiplier = 45.08f },
|
||||
RocheLimit = new ExponentialCurve { Exponent = .25f, Multiplier = 7.32f },
|
||||
BodyRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 3f },
|
||||
LightRadius = new ExponentialCurve()
|
||||
};
|
||||
|
||||
public sealed class World
|
||||
{
|
||||
public CultCache Cache;
|
||||
public ItemManager Items;
|
||||
public ZonePack Pack = new ZonePack { Radius = 20000f, Mass = 10000f };
|
||||
|
||||
public CultRecordKey Orbit(CultRecordKey parent, float distance, float phase = 0f)
|
||||
{
|
||||
var data = new OrbitData { Distance = distance, Phase = phase };
|
||||
if (parent.IsSet()) data.Parent = new CultRecordRef<OrbitData>(parent);
|
||||
var key = Cache.Upsert(data).Key;
|
||||
Pack.Orbits.Add(new CultRecordRef<OrbitData>(key));
|
||||
return key;
|
||||
}
|
||||
|
||||
public CultRecordKey Body(CultRecordKey orbit, float mass)
|
||||
{
|
||||
var key = Cache.Upsert(new PlanetData { Name = "Body", Orbit = new CultRecordRef<OrbitData>(orbit), Mass = mass }).Key;
|
||||
Pack.Planets.Add(new CultRecordRef<BodyData>(key));
|
||||
return key;
|
||||
}
|
||||
|
||||
public Zone Build() => new Zone(Items, Curves(), Pack, new GalaxyZone { Name = "OrbitsFromField", Owner = null }, null);
|
||||
}
|
||||
|
||||
public World New()
|
||||
{
|
||||
var dir = Path.Combine(Root, $"world{Caches.Count}");
|
||||
Directory.CreateDirectory(dir);
|
||||
var cache = AetheriaStores.Open(Path.Combine(dir, "Aetheria.cc"), Path.Combine(dir, "run.cc"), catalogWritable: true);
|
||||
Caches.Add(cache);
|
||||
return new World { Cache = cache, Items = new ItemManager(cache, new ProvenanceLedger(), GameSettings(), _ => { }) };
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue