Trojan swarms: a belt on a copy of its host's rail whose rocks ride the rail's period
Rulings trojans-first, trojans-ride-host-period-with-stations. AsteroidBeltData key 11 RidesRail; AsteroidBelt.Extent is the one belt extent rule (replaces Zone's inline farthest-rock loop and the Math.Max at the well node); a swarm's Periods are its rail's period. The generator draws swarms from a stream of its own (TrojanStream), so no other draw in the zone moves. Tuning defaults are the spec's proposals; the code default TrojanProbability 0 draws none. Existing annulus tests skip swarm rails. Builds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
67517f407f
commit
92d6153c2e
7 changed files with 83 additions and 19 deletions
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
Binary file not shown.
|
|
@ -126,6 +126,14 @@ public class ZoneGenerationSettings
|
|||
public ExponentialCurve AsteroidCount;
|
||||
public ExponentialLerp AsteroidRotationSpeed;
|
||||
|
||||
// Trojan swarms (ruling trojans-ride-host-period-with-stations): the chance a planet or gas giant with a body for a parent
|
||||
// carries one, each swarm's length along the orbit (turns) and radial half-width (a fraction of the host's distance), and
|
||||
// the rocks round each of L4 and L5 by host distance. 0 here: a settings object built in code draws none.
|
||||
public float TrojanProbability = 0f;
|
||||
public float TrojanArc = .07f;
|
||||
public float TrojanHalfWidth = .06f;
|
||||
public ExponentialCurve TrojanCount = new ExponentialCurve { Exponent = 1f, Multiplier = .04f, Constant = 8f };
|
||||
|
||||
public float ResourceDensityMinimum = .1f;
|
||||
public float ResourceDensityMaximum = 1.5f;
|
||||
|
||||
|
|
|
|||
|
|
@ -212,13 +212,11 @@ public class Zone
|
|||
foreach (var body in PlanetInstances)
|
||||
if (body.Value.WellStrength > 0)
|
||||
wellMass.TryAdd(Planets[body.Key].Orbit.Key, body.Value.BodyData.Mass);
|
||||
// A belt's rail carries its asteroids, which spread beyond the rail's own distance: its extent is the outermost.
|
||||
var beltExtent = new Dictionary<CultRecordKey, float>();
|
||||
foreach (var belt in AsteroidBelts)
|
||||
{
|
||||
var extent = 0f;
|
||||
foreach (var asteroid in belt.Value.Data.Asteroids) extent = Math.Max(extent, asteroid.Distance);
|
||||
beltExtent[Planets[belt.Key].Orbit.Key] = extent;
|
||||
var rail = Planets[belt.Key].Orbit.Key;
|
||||
beltExtent[rail] = belt.Value.Extent(Orbits[rail].Data.Distance);
|
||||
}
|
||||
var wellNodes = new List<WellNode>();
|
||||
foreach (var orbit in Orbits)
|
||||
|
|
@ -226,7 +224,7 @@ public class Zone
|
|||
var data = orbit.Value.Data;
|
||||
wellNodes.Add(new WellNode(orbit.Key, data.Parent.Key, data.Distance, data.Phase,
|
||||
wellMass.TryGetValue(orbit.Key, out var mass) ? mass : 0f,
|
||||
beltExtent.TryGetValue(orbit.Key, out var reach) ? Math.Max(reach, data.Distance) : data.Distance,
|
||||
beltExtent.TryGetValue(orbit.Key, out var reach) ? reach : data.Distance,
|
||||
data.FixedPosition + OrbitData.Evaluate(frac(data.Phase)) * data.Distance)); // a root's stationary place: Offset at an infinite period
|
||||
}
|
||||
Wells = WellTree.Build(wellNodes, settings, pack.Radius);
|
||||
|
|
@ -239,7 +237,9 @@ public class Zone
|
|||
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);
|
||||
belt.Value.Periods[i] = belt.Value.Data.RidesRail
|
||||
? Orbits[beltOrbit].Period // a swarm's rocks ride their rail (trojans-ride-host-period-with-stations)
|
||||
: PeriodAround(beltParent, belt.Value.Data.Asteroids[i].Distance, beltOrbit);
|
||||
}
|
||||
|
||||
PopulatedRadius = ComputePopulatedRadius();
|
||||
|
|
@ -882,6 +882,10 @@ public class AsteroidBelt
|
|||
Radius = data.Asteroids.Max(a => a.Distance);
|
||||
}
|
||||
|
||||
// What the belt's rail adds to its parent's L_F (WellTree): an annulus reaches its outermost rock, whose period reads the
|
||||
// field there; a swarm riding its rail reaches only the rail, whose period every rock shares.
|
||||
public float Extent(float railDistance) => Data.RidesRail ? railDistance : Math.Max(Radius, railDistance);
|
||||
|
||||
// Cut 1 (docs/mining-cut.md): the formula of Zone.cs:269-271 verbatim, `time` in double as `_time` was used.
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -105,6 +105,12 @@ public class AsteroidBeltData : BodyData
|
|||
// it on first load through FieldKinds.Ensure, the same function generation uses.
|
||||
[JsonProperty("kind"), Key(10)]
|
||||
public CultRecordRef<FieldKindData> Kind;
|
||||
|
||||
// A Trojan swarm (ruling trojans-ride-host-period-with-stations): every rock rides the belt's rail at the rail's period, so
|
||||
// the swarm keeps its shape. False on a belt from before this key (MessagePack leaves a missing trailing key at its default),
|
||||
// which keeps each rock on its own distance's period: an annulus.
|
||||
[JsonProperty("ridesRail"), Key(11)]
|
||||
public bool RidesRail;
|
||||
}
|
||||
|
||||
// Mining Cut 3 (docs/mining-cut-refresh.md, Q11): a kind of debris field, an authored catalog record. A chunk's
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ public static class ZoneGenerator
|
|||
var pack = new ZonePack();
|
||||
var cache = itemManager.ItemData;
|
||||
|
||||
var random = new Random(galaxyZone.Name.StableHash() ^ (uint) pcg3d(galaxyZone.Position).x);
|
||||
var seed = galaxyZone.Name.StableHash() ^ (uint) pcg3d(galaxyZone.Position).x;
|
||||
var random = new Random(seed);
|
||||
|
||||
var density = saturate(galaxy.Background.CloudDensity(galaxyZone.Position)/2);
|
||||
pack.Radius = zoneSettings.ZoneRadius.Evaluate(density);
|
||||
|
|
@ -214,6 +215,29 @@ public static class ZoneGenerator
|
|||
return cache.Upsert(planetData);
|
||||
}).ToList();
|
||||
|
||||
// Trojan swarms (rulings trojans-first, trojans-ride-host-period-with-stations): a belt on a copy of its host's rail, its
|
||||
// rocks round both L4 and L5. Drawn from a stream of its own, so no other draw in the zone moves; not a GeneratorPlanet,
|
||||
// so it takes no slot and no station candidacy sees it.
|
||||
var swarmRandom = new Random(seed ^ TrojanStream);
|
||||
foreach (var host in planets.Where(p => p.Parent != null && !p.Parent.Empty && !p.Empty && !p.Belt && p.Distance > 0
|
||||
&& p.Mass > zoneSettings.PlanetMass && p.Mass <= zoneSettings.SunMass))
|
||||
{
|
||||
if (swarmRandom.NextFloat() >= zoneSettings.TrojanProbability) continue;
|
||||
var hostOrbit = orbitMap[host];
|
||||
var rail = new OrbitData { Parent = hostOrbit.Parent, Distance = hostOrbit.Distance, Phase = hostOrbit.Phase };
|
||||
pack.Orbits.Add(cache.Upsert(rail));
|
||||
var swarm = new AsteroidBeltData
|
||||
{
|
||||
Orbit = cache.RefOf(rail), RidesRail = true,
|
||||
Asteroids = TrojanRocks(zoneSettings, ref swarmRandom, rail.Distance, rail.Phase)
|
||||
};
|
||||
BodyData swarmBody = swarm;
|
||||
var swarmKey = cache.Upsert(swarmBody).Key;
|
||||
swarm.Name = swarmKey.Value.Substring(0, 8);
|
||||
FieldKinds.Ensure(swarmKey, swarm, cache);
|
||||
pack.Planets.Add(cache.Upsert(swarmBody));
|
||||
}
|
||||
|
||||
// The generator's own well tree, for clearance only (the Zone builds its own): what a rail must stay out of is the body's
|
||||
// support, or its Roche margin if more.
|
||||
var wellNodes = planets.Select(p => new WellNode(cache.RefOf(orbitMap[p]).Key, p.Parent == null ? default : cache.RefOf(orbitMap[p.Parent]).Key,
|
||||
|
|
@ -577,6 +601,24 @@ public static class ZoneGenerator
|
|||
// limits out; a massive child of a body also far enough that its own pull peak beats CuspMargin times its parent's pull there;
|
||||
// siblings sit SiblingSpacing of a reach apart. Every other number is a GravityField constant.
|
||||
public const float RocheMargin = 1.5f;
|
||||
|
||||
private const uint TrojanStream = 0x9E3779B9u;
|
||||
|
||||
// A swarm's rocks: alternately round L4 and L5 of the rail at `distance` and `phase`, each within half an arc of its point
|
||||
// along the orbit and a half-width of the rail across it, thickest at the point.
|
||||
public static Asteroid[] TrojanRocks(ZoneGenerationSettings settings, ref Random random, float distance, float phase)
|
||||
{
|
||||
var rocks = new Asteroid[2 * (int) settings.TrojanCount.Evaluate(distance)];
|
||||
for (var i = 0; i < rocks.Length; i++)
|
||||
rocks[i] = new Asteroid
|
||||
{
|
||||
Distance = distance * (1 + random.NextFloat() * (random.NextFloat() - .5f) * 2 * settings.TrojanHalfWidth),
|
||||
Phase = frac(phase + (i % 2 == 0 ? 1f : -1f) / 6f + random.NextFloat() * (random.NextFloat() - .5f) * settings.TrojanArc),
|
||||
Size = random.NextFloat(),
|
||||
RotationSpeed = settings.AsteroidRotationSpeed.Evaluate(random.NextFloat())
|
||||
};
|
||||
return rocks;
|
||||
}
|
||||
public const float CuspMargin = 1.5f;
|
||||
public const float SiblingSpacing = .5f;
|
||||
|
||||
|
|
|
|||
|
|
@ -79,11 +79,11 @@ public sealed class ChildrenWithinReachTests : IClassFixture<ChildrenWithinReach
|
|||
private static Dictionary<CultRecordKey, float> BodyMass(Zone zone) =>
|
||||
zone.Planets.Values.Where(b => !(b is AsteroidBeltData)).GroupBy(b => b.Orbit.Key).ToDictionary(g => g.Key, g => g.First().Mass);
|
||||
|
||||
// The generated orbits: every body's and its ancestors', which leaves out stations, turrets and Lagrange copies.
|
||||
// The generated orbits: every body's and its ancestors', which leaves out stations, turrets, Lagrange copies and swarm rails (a copy of the host's rail, like a Lagrange copy).
|
||||
private static HashSet<CultRecordKey> Placed(Zone zone)
|
||||
{
|
||||
var placed = new HashSet<CultRecordKey>();
|
||||
foreach (var body in zone.Planets.Values)
|
||||
foreach (var body in zone.Planets.Values.Where(b => !(b is AsteroidBeltData { RidesRail: true })))
|
||||
for (var key = body.Orbit.Key; placed.Add(key) && zone.Orbits[key].Data.Parent.IsSet();) key = zone.Orbits[key].Data.Parent.Key;
|
||||
return placed;
|
||||
}
|
||||
|
|
@ -332,7 +332,7 @@ public sealed class ChildrenWithinReachTests : IClassFixture<ChildrenWithinReach
|
|||
foreach (var (seed, zone) in _zones.All)
|
||||
{
|
||||
var mass = BodyMass(zone);
|
||||
foreach (var (beltKey, belt) in zone.AsteroidBelts)
|
||||
foreach (var (beltKey, belt) in zone.AsteroidBelts.Where(b => !b.Value.Data.RidesRail))
|
||||
{
|
||||
belts++;
|
||||
var orbit = zone.Planets[beltKey].Orbit.Key;
|
||||
|
|
@ -374,7 +374,7 @@ public sealed class ChildrenWithinReachTests : IClassFixture<ChildrenWithinReach
|
|||
{
|
||||
var mass = BodyMass(zone);
|
||||
var placed = Placed(zone);
|
||||
var beltAt = zone.AsteroidBelts.ToDictionary(b => zone.Planets[b.Key].Orbit.Key, b => b.Value.Data.Asteroids);
|
||||
var beltAt = zone.AsteroidBelts.Where(b => !b.Value.Data.RidesRail).ToDictionary(b => zone.Planets[b.Key].Orbit.Key, b => b.Value.Data.Asteroids);
|
||||
foreach (var (orbit, rocks) in beltAt)
|
||||
{
|
||||
var parent = zone.Orbits[orbit].Data.Parent.Key;
|
||||
|
|
@ -424,7 +424,7 @@ public sealed class ChildrenWithinReachTests : IClassFixture<ChildrenWithinReach
|
|||
{ Exponent = authored.Exponent, Multiplier = authored.Multiplier * .02f, Constant = authored.Constant * .02f });
|
||||
int belts = 0, filled = 0;
|
||||
foreach (var (seed, zone) in narrow.All)
|
||||
foreach (var (key, belt) in zone.AsteroidBelts)
|
||||
foreach (var (key, belt) in zone.AsteroidBelts.Where(b => !b.Value.Data.RidesRail))
|
||||
{
|
||||
var rocks = belt.Data.Asteroids;
|
||||
if (rocks.Length < 2) continue;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ public sealed class StationPlacementTests : IDisposable
|
|||
|
||||
private static float Turns(float turns) => turns - (float) Math.Floor(turns);
|
||||
|
||||
// A Trojan swarm's rail is a copy of its host's, not a station's source.
|
||||
private static bool SwarmRail(Zone zone, CultRecordKey orbit) =>
|
||||
zone.Planets.Values.Any(b => b is AsteroidBeltData { RidesRail: true } && b.Orbit.Key.Equals(orbit));
|
||||
|
||||
[Fact]
|
||||
public void AStationSitsAtItsPlanetsL4OrL5()
|
||||
{
|
||||
|
|
@ -96,7 +100,7 @@ public sealed class StationPlacementTests : IDisposable
|
|||
foreach (var station in zone.Entities.OfType<OrbitalEntity>().Where(e => TypeOf(built.Items, e) == HullType.Station))
|
||||
{
|
||||
var orbit = zone.Orbits[station.OrbitData];
|
||||
var sources = zone.Orbits.Where(o => !ridden.Contains(o.Key) && SameRing(o.Value, orbit)).ToList();
|
||||
var sources = zone.Orbits.Where(o => !ridden.Contains(o.Key) && !SwarmRail(zone, o.Key) && SameRing(o.Value, orbit)).ToList();
|
||||
if (sources.Count == 0)
|
||||
{
|
||||
// The tutorial entrance's one exception: a station on its own orbit round a root node, beyond the node's ring
|
||||
|
|
@ -156,7 +160,7 @@ public sealed class StationPlacementTests : IDisposable
|
|||
foreach (var station in Orbitals(built).Where(e => TypeOf(built.Items, e) == HullType.Station))
|
||||
{
|
||||
var orbit = zone.Orbits[station.OrbitData];
|
||||
var sources = zone.Orbits.Where(o => !ridden.Contains(o.Key) && SameRing(o.Value, orbit)).ToList();
|
||||
var sources = zone.Orbits.Where(o => !ridden.Contains(o.Key) && !SwarmRail(zone, o.Key) && SameRing(o.Value, orbit)).ToList();
|
||||
if (sources.Count == 0) continue; // the tutorial entrance's own orbit round a root
|
||||
stations++;
|
||||
Assert.Contains(bodyOrbits, key => key.Equals(sources[0].Key));
|
||||
|
|
@ -173,12 +177,12 @@ public sealed class StationPlacementTests : IDisposable
|
|||
var placed = new HashSet<CultRecordKey>();
|
||||
foreach (var body in zone.Planets.Values)
|
||||
for (var key = body.Orbit.Key; placed.Add(key) && zone.Orbits[key].Data.Parent.IsSet();) key = zone.Orbits[key].Data.Parent.Key;
|
||||
var beltExtent = zone.AsteroidBelts.ToDictionary(b => zone.Planets[b.Key].Orbit.Key, b => b.Value.Data.Asteroids.Select(a => a.Distance).DefaultIfEmpty(0f).Max());
|
||||
var beltExtent = zone.AsteroidBelts.ToDictionary(b => zone.Planets[b.Key].Orbit.Key, b => b.Value.Extent(zone.Orbits[zone.Planets[b.Key].Orbit.Key].Data.Distance));
|
||||
var nodes = placed.Select(k =>
|
||||
{
|
||||
var d = zone.Orbits[k].Data;
|
||||
return new WellNode(k, d.Parent.Key, d.Distance, d.Phase, mass.TryGetValue(k, out var m) ? m : 0f,
|
||||
beltExtent.TryGetValue(k, out var e) ? Math.Max(e, d.Distance) : d.Distance, d.FixedPosition + OrbitData.Evaluate(frac(d.Phase)) * d.Distance);
|
||||
beltExtent.TryGetValue(k, out var e) ? e : d.Distance, d.FixedPosition + OrbitData.Evaluate(frac(d.Phase)) * d.Distance);
|
||||
}).ToList();
|
||||
var wells = WellTree.Build(nodes, zone.Settings, zone.Pack.Radius);
|
||||
return Math.Max(wells.Support(orbit), 1.5f * zone.Settings.RocheLimit.Evaluate(mass[orbit]));
|
||||
|
|
@ -353,7 +357,7 @@ public sealed class StationPlacementTests : IDisposable
|
|||
foreach (var station in Orbitals(built).Where(e => TypeOf(built.Items, e) == HullType.Station))
|
||||
{
|
||||
var orbit = zone.Orbits[station.OrbitData];
|
||||
var strays = zone.Orbits.Count(o => !ridden.Contains(o.Key) && SameRing(o.Value, orbit));
|
||||
var strays = zone.Orbits.Count(o => !ridden.Contains(o.Key) && !SwarmRail(zone, o.Key) && SameRing(o.Value, orbit));
|
||||
Assert.True(strays == 1, $"seed {built.Seed}: a station's ring holds {strays} orbits no entity rides, not just its source's");
|
||||
stations++;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue