From 781607763e857fa95919d8af4a009d0b7381407e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 04:55:48 +0200 Subject: [PATCH] WellTree: SubzoneReach is one public static; the strengthless-orbit mean rule is deleted The nearest-anchor rule (Reach x distance to the nearest other root) moves to WellTree.SubzoneReach(anchors, root) so the generator spells it once; the instance method returns max(it, floor). PullAround no longer gives a strengthless orbit on a ring a period of its own (binaries-orbited-from-afar); its test goes with it. Builds. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/WellTree.cs | 40 ++++++++++--------- .../OrbitsFromFieldTests.cs | 37 +---------------- 2 files changed, 23 insertions(+), 54 deletions(-) diff --git a/Assets/Scripts/ServerShared/WellTree.cs b/Assets/Scripts/ServerShared/WellTree.cs index bacf474de..5f9f408e6 100644 --- a/Assets/Scripts/ServerShared/WellTree.cs +++ b/Assets/Scripts/ServerShared/WellTree.cs @@ -257,19 +257,29 @@ public sealed class WellTree } } - // A root's reach. The one place the subzone shape is decided (question multi-root-zone-shape): a sole massive root is the - // whole zone (+inf, so its global term is the far field); with several, isolated-subzones bounds each root to Reach x the - // distance to the nearest other massive root, or its floor (Beta x support) if more. The superposed-inside option - // (Beta x max(pack radius, max over massive roots of |anchor - Centre| + support)) is not a change to this return: it needs - // every massive root's support, and roots are processed one at a time here, so it would take a second pass over the roots - // once their supports exist. + // A root's reach: the floor (Beta x support) or the subzone reach, whichever is more. The subzone shape is decided in the + // public static SubzoneReach (multi-root-isolated-subzones): the generator calls the same function, so Reach x the nearest + // root's distance is spelled once and the roots' windows are fitted to it (root-window-floor). private float SubzoneReach(int root, float floor) { - if (_massiveRoots.Length < 2) return float.PositiveInfinity; + var anchors = new List(); + foreach (var other in _massiveRoots) anchors.Add(_anchor[other]); + return Math.Max(SubzoneReach(anchors, Array.IndexOf(_massiveRoots, root)), floor); + } + + /// + /// A subzone root's reach among the zone's massive roots' anchors: a sole massive root is the whole zone (+inf, so its + /// global term is the far field); with several, each root is bounded to GravityField.Reach x the distance to the nearest + /// other root (multi-root-isolated-subzones). The superposed-inside option is not a change to this return: it needs every + /// root's support, and roots are processed one at a time. + /// + public static float SubzoneReach(IReadOnlyList anchors, int root) + { + if (anchors.Count < 2) return float.PositiveInfinity; var nearest = float.PositiveInfinity; - foreach (var other in _massiveRoots) - if (other != root) nearest = Math.Min(nearest, length(_anchor[other] - _anchor[root])); - return Math.Max(GravityField.Reach * nearest, floor); + for (var other = 0; other < anchors.Count; other++) + if (other != root) nearest = Math.Min(nearest, length(anchors[other] - anchors[root])); + return GravityField.Reach * nearest; } private void EmitTerms(int[] parent, float[] body, float[] family, int[][] ring, float[] lower, RingGroup[] group, @@ -395,8 +405,7 @@ public sealed class WellTree /// /// The pull (toward the node, u/s^2) a rail at `distance` round `parentOrbit` is given: the node's own terms at that /// distance. A ring member of a bodyless node adds each other member's own terms at their chord (positions at t = 0), - /// projected toward the node; a strengthless orbit on that ring (a station, a turret) takes the mean of what the ring's - /// members feel, so it keeps their period. Infinite period for a pull of 0 is the caller's. + /// projected toward the node. Infinite period for a pull of 0 is the caller's. /// public float PullAround(CultRecordKey parentOrbit, float distance, CultRecordKey self) { @@ -405,12 +414,7 @@ public sealed class WellTree var members = _ring[node]; if (members == null || members.Length < 2) return pull; if (_index.TryGetValue(self, out var me) && Array.IndexOf(members, me) >= 0) return pull + PartnersPull(members, me); - var inner = float.PositiveInfinity; - foreach (var m in members) inner = Math.Min(inner, _distance[m]); - if (Math.Abs(distance - inner) > RingWindow) return pull; - var mean = 0f; - foreach (var m in members) mean += PartnersPull(members, m) / members.Length; - return pull + mean; + return pull; } // What the other members of a ring pull member m with, along the direction from m to the node. diff --git a/tests/Aetheria.Shared.Tests/OrbitsFromFieldTests.cs b/tests/Aetheria.Shared.Tests/OrbitsFromFieldTests.cs index fea0d84c6..6a3b1c618 100644 --- a/tests/Aetheria.Shared.Tests/OrbitsFromFieldTests.cs +++ b/tests/Aetheria.Shared.Tests/OrbitsFromFieldTests.cs @@ -150,41 +150,6 @@ public sealed class OrbitsFromFieldTests : IDisposable Assert.Equal(zone.Orbits[planet2].Period, zone.Orbits[companion2].Period); } - // A station on a binary's ring, with nothing inside, feels what the pair's members feel (the mean of what each does), - // and so does one a hair off the ring's distance but within its window. - [Theory] - [InlineData(0f)] - [InlineData(.05f)] - [InlineData(-.05f)] - public void ACompanionOnABinarysRingTakesTheMembersPeriod(float offset) - { - var w = NewWorld(); - var node = w.Orbit(default, 0f); - var light = w.Orbit(node, 400f, 0f); - var heavy = w.Orbit(node, 400f, .5f); - var lightBody = w.Body(light, 100f); - var heavyBody = w.Body(heavy, 2000f); - var companion = w.Orbit(node, 400f + offset, .25f); - var zone = w.Build(); - - Assert.True(C(zone, heavyBody) > 1.5f * C(zone, lightBody), "fixture: unequal members"); - AssertPeriod(PeriodUnder((Pull(zone, lightBody, 800f) + Pull(zone, heavyBody, 800f)) / 2, 400f + offset), zone.Orbits[companion].Period, - "the companion, between its two members"); - - // An equal pair: the companion has the members' period. - w = NewWorld(); - node = w.Orbit(default, 0f); - var a = w.Orbit(node, 400f, 0f); - var b = w.Orbit(node, 400f, .5f); - w.Body(a, 300f); - w.Body(b, 300f); - companion = w.Orbit(node, 400f, .25f); - zone = w.Build(); - Assert.True(float.IsFinite(zone.Orbits[a].Period)); - AssertPeriod(zone.Orbits[a].Period, zone.Orbits[companion].Period, "the companion, against the first member"); - AssertPeriod(zone.Orbits[b].Period, zone.Orbits[companion].Period, "the companion, against the second member"); - } - // The ring is the 0.1 u window about the innermost massive distance: a member just inside it is a ring partner (felt at // the chord), one just outside it is not a partner and the rail has the node's own terms alone, which give a // ring's own member next to no pull. @@ -291,7 +256,7 @@ public sealed class OrbitsFromFieldTests : IDisposable authored.Read("NameGeneratorSettings"), cache, new PlayerSettings(), Directory.CreateDirectory(Path.Combine(dir, "Narrative")), _ => { }, null, seed); var items = new ItemManager(cache, new ProvenanceLedger(), authored.Read("GameplaySettings"), _ => { }); - var pack = ZoneGenerator.GenerateZone(items, authored.Read("ZoneSettings"), galaxy, galaxy.Entrance, + var pack = ZoneGenerator.GenerateZone(items, authored.Read("ZoneSettings"), authored.Read("PlanetSettings"), galaxy, galaxy.Entrance, galaxy.IsPrelude, false); var zone = new Zone(items, authored.Read("PlanetSettings"), pack, galaxy.Entrance, galaxy);