diff --git a/docs/aetheria-gravity-map.md b/docs/aetheria-gravity-map.md index 18a173d9e..7f38cb6f7 100644 --- a/docs/aetheria-gravity-map.md +++ b/docs/aetheria-gravity-map.md @@ -72,6 +72,10 @@ All read at c6519383 in the detached worktree `C:\aeth-wt\ag2-docs` on 2026-10-1 - **B32. The ship is drawn ahead in xz but not in y** (source read at 8f491fd3, 2026-10-15, after the operator's flight: "the new jitter is showing up when moving up a steep slope, not necessarily on those texel boundaries"). The sim steps at a fixed 60 steps per real second (`ActionGameManager.Clock = new SimClock(1f / 60f)`, ActionGameManager.cs:251; `Clock.Advance` runs whole steps, :1378). `Entity.Update` sets `Position.y = Zone.GetHeight(Position.xz) + hullData.GridOffset` once per step (Entity.cs:1893). Presenters draw `Lead` sim seconds past the last step (SimClock.Lead, 0 <= Lead < Step); `DrawAhead.Position(Entity, lead)` (DrawAhead.cs:17-20) adds `Velocity * lead` to x and z and leaves y at the last step's height, and `EntityInstance` writes that to its transform every frame (EntityInstance.cs:405-406). The follow camera's `Follow` is that transform and its `LookAt` is `LookAtPoint`, placed from the same transform (ActionGameManager.cs:1071-1072, EntityInstance.cs:396; CinemachineBrain update method 2, LateUpdate, ARPG.unity:30782). So on a slope the drawn ship and both camera targets move smoothly across and in steps up: each step's y jump is slope x speed x Step, and a frame that runs 0, 1 or 2 steps shows 0, 1 or 2 jumps. At 100 u/s on a drawn slope of 0.5 that is 0.83 u per step against a 70 u camera distance; at K = 4.098 every slope outside the cores is 2.44x what it was at K = 10 (B31), so the steps are 2.44x taller. `SimClockTests.DrawAheadIsTheStateAtZeroLead` pins the defect (`Assert.Equal(player.Position.y, ahead.y)`, SimClockTests.cs:267), vacuously true in its flat duel arena. Everything else on screen (grid, nebula, stardust, planets) is drawn at the last step's state, so it is the camera that steps, which is why the whole view jitters; why planets look worst is not established by source read (candidates: the LookAt composer turns the camera with each step and planets are the large sharp-edged objects far below; or their own heights). Not reproduced in Unity. - **B33. The nebula patch draws gravity a second time, in half floats at 32 u.** Patch Displacement (ARPG.unity camera 507065483, `m_Bits: 5632` = layers 9, 10, 12, line 8040) renders the Gravity Field quad (layer 9) into `Fog Patch Displacement.renderTexture`, 256² `m_ColorFormat: 45` (R16_SFloat), over the same 2·ViewDistance window as Surface Displacement, so 32 u texels. Its one reader is `cloudDensity` (Volumetric.cginc:95-104): `patchDensity = saturate((-abs(pos.y + patchDisp - _NebulaFloorOffset) + patch) / _NebulaPatchBlend) * _NebulaPatchDensity`, with `PatchBlend: 25` in the default environment (Settings.asset:378). A half float's step is 0.5 u from depth 512, 1 u from 1024, 2 u from 2048 and 4 u from 4096; at K = 4.098 the median cone is about -1160 and the worst about -4500 (B31), so the patch surface is terraced in 1-4 u steps, 4-16% of the 25 u blend, where at K = 10 (median -476, B28) the steps were 0.25-0.5 u. The patch's caller already holds the full-precision gravity height: `density` samples `_NebulaSurfaceHeight` (R32, 4 u) at the same uv and passes it in as `surfaceDisp` (Volumetric.cginc:174, 224). No other code reads `_NebulaPatchHeight` (`Stardust.cs:31` holds the texture and never binds it). Shader precision model is platform default (ProjectSettings.asset:263), so `half4(uv, ...)` is full float on desktop and not a cause. Not confirmed in Unity that the operator's bands are these terraces rather than the 32 u bilinear facets of the same texture; the cut below removes both. - **B34. The fog cameras are already texel-snapped.** `FogRenderers` (parent of Surface Displacement, Patch Displacement and Patch) carries `CopyTransform` with `PixelSnapCamera` = the Patch camera (512²) and `PixelSnapIncrement 4`, so it follows Main Camera in xz snapped to `orthographicSize * 4 / 512` = 32 u (CopyTransform.cs, ARPG.unity FogRenderers), a whole number of texels of every fog target (8 of Surface Displacement, 1 of Patch Displacement, 2 of Fog Patch). Execution order ZoneRenderer 50, CopyTransform 51, VolumeSampling 52 (their .cs.meta), so `_GridTransform` is set after the snap in the same frame. The texel-boundary jitter the operator saw is older and separate: "I have felt that grid texel jitter before, that only showed up for me when I brought back the liquid grid shader (CatmullGrid object in ARPG scene), couldn't figure out how to get that to match, even when reading the same offsets the marcher uses and applying the translation in-shader." No CatmullGrid object exists in ARPG.unity at 8f491fd3; it is follow-up `catmullgrid-texel-jitter`. +- **B35. Orbit speeds and periods under the field** (`o1.py`, `o2.py` in the Imagination scratchpad `imagination-ag2-orbits-from-field/`, 2026-10-15, over `docs/research/2026-10-13-orbit-probe/data/bodies.csv`; ZoneGenerator is unchanged since c8d40eaf, 2026-10-07, so the distribution holds at 8f491fd3; c = 45.08·m^0.2, Roche limit and core 7.32·m^0.25). Rule probed: a child's rail obeys its parent's own well, a = |GravityField.Acceleration(c_p, R_p, d)|, v = sqrt(a·d), T = 2π·sqrt(d/a); a bodyless parent by the ring law of B37. Circular speed by parent class, p10/p50/p90 (n): sun 16.5/20.2/24.5 u/s (16613, root and non-root suns), gas giant 13.6/14.6/15.5 (6226), planet 10.9/12.2/13.3 (3417), planetoid 10.1/10.5/10.6 (300); bodyless parents: rosette suns 11.2/42.7/68.7 (1338: outer planets of a root rosette enclose several suns), gas giant 8.7/11.7/32.3 (844), planet 6.8/7.7/9.6 (1248), planetoid 6.1/6.8/7.3 (154). Today every rail runs at 2π = 6.28 u/s (T = d). Periods: p10/p50/p90 47/151/621 s against 100/389/2560 s today, a median 2.65x shorter. Station orbits (Lagrange candidates from the middle of each zone's distance order, a proxy for `selectedStationOrbits`, ZoneGenerator.cs:241): 13.9/16.7/23.5 u/s, max 48.5. Children inside their parent's Roche limit, hence inside its core: 234 of 30,140 (0.8%; planetoids 214, planets 9, belts 11); their period from the core law is 2πR/sqrt(c), finite and independent of d (a solid-body rail), so the orbit stays well defined; children-within-reach moves them out. With no rule for bodyless parents, 3584 children (12%) would have no field and stand still. +- **B36. Readers of the period and of orbit velocity at 8f491fd3.** Period writers: `Orbit`'s constructor (Zone.cs:849-853, `settings.OrbitPeriod.Evaluate(data.Distance)`), `AsteroidBelt.Pose` per asteroid per call (Zone.cs:765), `BeltTargets` (BeltTargets.cs:63, which must divide by the same float Pose does). Readers of `Orbit.Period`: `GetOrbitPosition` (Zone.cs:484-486, which moves an orbit only when Period > .01) and three tests (ZonePlacedAtBuildTests.cs:113, 137, 143; RunStartTests.cs:325). `Zone.CreateOrbit` is gone (B17's dead writer was deleted). Orbits are constructed (Zone.cs:159-162) and placed (:167-168) before any body exists (:170-195), and `BeltTargets` is built inside the body loop (:181), so a period that reads a parent's well needs the bodies first: construct orbits, build bodies, derive periods, then place and build belt targets. Orbit velocity is a difference of placed positions (Zone.cs:283-287); `OrbitalEntity.Update` copies it to stations and turrets (OrbitalEntity.cs:30-31) and a docked ship copies its parent's (Entity.cs:1891). Docking checks distance only, 25 u (ActionGameManager.cs:896-897, `DockingDistance`), never relative speed; undocking keeps the station's velocity. AI `MoveToOrbitState` chases the orbit's position at TopSpeed (100) with no lead (MoveTo.cs:18-38); turrets lead targets by `target.Velocity` (TurretController.cs:68). So faster rails (B35) break no mechanism: a pilot holds within 25 u of a station moving 14-24 u/s (48 at most), where today it moves 6.3. No Unity-side code reads `OrbitPeriod` or `Period` (git grep). Tests that set `OrbitPeriod`: GravitySubtractTests.cs:49, MiningCut1Tests.cs:40, MiningCut2Tests.cs:43, MiningCut3Tests.cs:54, MiningIndexTests.cs:461, PotentialOwnerTests.cs:49, 86, 141, WellFixture.cs:13, ZonePlacedAtBuildTests.cs:45; MiningCut1Tests.cs:100-116 re-implements the curve. Fixtures whose parents carry no well (they move today only because the curve ignores the parent): ZonePlacedAtBuildTests (bodyless root; a bodyless moon under the moon's moon), MiningCut1Tests (a parentless root orbit at Distance 4000 that circles its FixedPosition, a shape the generator never makes: generated roots sit at distance 0), and MiningCut3Tests and MiningIndexTests (belts round a SunData of mass 0 under a PlanetSettings with no WellStrength, so c = 0). c = 4π² gives T = d exactly outside the core, today's game periods. The Lagrange station orbit adds PI/3 to Phase (ZoneGenerator.cs:264), but Phase is in turns (`OrbitData.Evaluate`, ZoneData.cs:234-237), so a station sits 0.047 turns (17°) from its planet, not at L4 or L5 (follow-up `lagrange-phase-in-turns`). +- **B37. Bodyless parents and the ring law.** ZoneGenerator's `ExpandRosette` (ZoneGenerator.cs:551-592) leaves the parent node empty and puts n children on one circle at phases i/n; binaries are rosettes of two (:484-487), roots can be rosettes of suns (the probe's 321 empty root suns), and a later `ExpandSolar` on an empty node adds outer children beyond the ring. In 2D a point of strength c_j at distance s on the same circle pulls a ring member inward by exactly c_j/(2d) (radial part of c_j/s with s² = 2d²(1-cos θ)), so a ring member's rail is v² = ½·Σ_{j≠i} c_j; and by 2D Gauss's law, which holds for log potentials, a ring exerts c_ring/d on anything outside it and nothing on anything inside it. So the field gives every child of an empty node a definite pull: the strength of its siblings strictly inside its distance, each with its whole family (a sibling's moons orbit with it), plus half the strength of siblings on its own circle (within 0.1 u, the generator's own rosette test, ZoneGenerator.cs:227) when the child itself carries strength. The last clause keeps a bodyless companion (a station or turret orbit at a planet's distance) on its planet's period: neither the planet nor its companion counts the other. Probed: no child of a generated zone is left without a field under this rule (B35), and nested binaries (an empty binary member that holds its own binary) get the ring term through their family's strength. +- **B38. Parent-only rails against the summed field** (`o1.py`). By 2D Gauss's law (B37) the phase-averaged pull at a child's distance from a body parent is the parent plus every sibling inside that distance, each with its family; siblings outside pull nothing on average. The circular speed of that enclosed field over the parent-only rail speed (B35), p10/p50/p90/p99, by the child's class: suns (children of a root or rosette) 1.44/1.98/2.44/2.75, gas giants 1.00/1.58/2.14/2.55, planets 1.00/1.21/1.65/2.05, planetoids 1.00/1.00/1.37/1.61, belts 1.00/1.14/1.49/1.88. Enclosed speeds: suns' children 24.4/46.0/60.3 u/s, planets 11.0/21.8/32.3. So under the funnel the operator kept (`funnel-kept-until-flown`), a ship coasting free at a body's orbital radius, away from the body, needs 1.2-2x the body's rail speed; patched conics' premise, that the parent dominates, is weak where c ∝ m^0.2 (the root holds 18% of a zone's total c, B19). The enclosed rule would also break field-strength-ruled's 12-18 u/s for planets round a median sun. Question `aetheria-gravity:question:rails-enclosed-or-parent` carries the fork; the two options differ by one term in one function. ## Model page @@ -83,7 +87,7 @@ One row per kind this campaign touches. "Today" is c6519383; "target" is the sha | **Force** a = -∇φ | Derived, never stored. | Per query. Analytic gradient (sum of per-well closed forms), not finite differences of the height. | Owner: `Zone.GetForce` over `GravityField.Acceleration`. Every CPU force reader (B5) calls it: ships, floating bodies, clouds, mines, and Zone.Flow for its downhill. `GetNormal` becomes derived from it and K, for ship tilt only. Forbidden: `Ship.Update`'s private normal-to-force path; `GravityAcceleration`, `GravityReferenceRate`, `GravityStrength`. | | **Body well parameters** (gravitational parameter c, softening radius R) | Derived per body from `BodyData.Mass` through PlanetSettings curves (today: `GravityDepth`, `GravityRadius`, `BodyRadius` × per-body multipliers). Names: per-body record key (`CultRecordRef`), injective. The JSON names of GasGiantData's wave fields collide with BodyData's (B7). | Computed once at Planet construction (`CalculateProperties`); never revised in play. Today's `GravityDepthExponent`, `GravityDepthMultiplier`, `GravityRadiusMultiplier` have no writer; the exponent dies under the target (no PowerPulse well); wave fields die in flatspace. | Owner of the curves: PlanetSettings `WellStrength` (c = 45.08·m^0.2, B19), and `RocheLimit` (7.32·m^0.25, the closest a child orbits, which is also the well's core radius: B31, core-at-roche-and-exaggeration-up) in `Settings.asset`. Owner of the per-body value: `Planet.WellStrength` and `RocheLimit`, set at construction (a positive, finite Roche limit is checked there), read later by orbits-from-field and children-within-reach; never authored per body. `GravityRadius`, `GravityDepth`, `Planet.GravityWellRadius/Depth` and the two BodyData multipliers (keys 6-7) fed the brushes only and are deleted by cut grid-lowering; keys 6-7 join the retired keys. Forbidden writers: the brush material's `_Power 16` and `_Cutoff`, ZoneRenderer copying depth into materials, any shader constant. Cell not filled: whether per-body multipliers stay (no writer today; a pillar or authored body may want one). | | **Pillar** (Ink zones) | The zone's central body in a sun's role (`ink-pillars-are-suns`). Cell not filled: no record names a pillar today (no `Pillar` or `Adrasteia` anywhere under Assets/Scripts at c6519383); whether it becomes a `BodyData` kind is the Ink-zone work's decision. | Built with the zone like any body. | Owner: same law as a sun (`pillars-are-suns`); its strength comes from the same curve or an authored c. Pseudospace shenanigans are not in scope. | -| **Orbit rail** (`OrbitData`: Distance, Phase, Parent, FixedPosition; runtime `Orbit`: Period, Position, PreviousPosition, Velocity) | `OrbitData` is a catalog/pack record keyed by `CultRecordKey`, parent by `CultRecordRef`; root has no parent. Period is derived, not stored. | OrbitData created by ZoneGenerator only (the run-time writers `AddOrbit` and `CreateOrbit` are dead, B17, and go in cut-orbits-placed-at-build); Period computed at `Orbit` construction; Position written by `GetOrbitPosition` each Update and float2.zero before the first (follow-up). Asteroids and belt targets recompute periods from the same curve (B6). | Today: `OrbitPeriod` curve owns period and ignores the field. Target (open: rails-vs-field, recommended field-owns-rails): period derived from the parent's c, T = 2πd/sqrt(c_parent), the same derivation for the four readers in B6. Forbidden writers then: `OrbitPeriod` as an independent authority, any per-reader period formula. Position placement moves to construction (`zone-valid-before-tick`). | +| **Orbit rail** (`OrbitData`: Distance, Phase, Parent, FixedPosition; runtime `Orbit`: Period, Position, PreviousPosition, Velocity) | `OrbitData` is a pack record keyed by `CultRecordKey`, parent by `CultRecordRef`; root has no parent. Period is derived, never stored or authored. | OrbitData created by ZoneGenerator only (the run-time writers are deleted); placed at build (`zone-valid-before-tick`), then each Update. Period derived once at build, after the bodies exist (B36). Asteroids get one period each, held by their belt and read by both Pose and BeltTargets. | Owner (cut orbits-from-field): `Zone.PeriodAround(parent, distance, self)` = 2π·sqrt(d/a), a from the parent's own well through `GravityField.Acceleration` (core included), or for a bodyless parent the ring law (B37); no field, no motion. A moon's period reads only its parent: the hierarchical (patched-conics) approximation, with its weak premise stated in B38 and the fork in question rails-enclosed-or-parent. Forbidden writers: `PlanetSettings.OrbitPeriod` (deleted), any per-reader period formula (Orbit's constructor, AsteroidBelt.Pose, BeltTargets), the grandparent's or the zone's strength. | | **Orbit placement** (child distances) | Generated `OrbitData.Distance` per body, authored ZoneGenerationSettings ranges. | Written once at generation; persisted in the zone pack. | Owner: ZoneGenerator. Open: moons-beyond-tidal-reach (whether the generator must keep children within their parent's tidal reach, which makes the generator read the strength curve). | | **Zone bowl** | PlanetSettings `ZoneDepth`, `ZoneDepthExponent`, `ZoneBoundaryFog`; scene `Boundary` brush. | Live today (B2). Target: gone from flatspace gravity. | Ruled (`grid-shows-how-you-move`, `flatspace-no-bowl-no-ripples`). Forbidden: the GetHeight term, ZoneRenderer's driving of the Boundary brush on a gravity layer, FlowFieldTests' use of the bowl as a slope source (tests need a well instead). Cell not filled: whether the zone needs a non-gravitational edge (nothing asked for one). | | **Ripples** (gas giant and sun waves) | PlanetSettings `WaveDepth`, `WaveRadius`, `WaveFrequency`, `WaveSpeed`; GasGiantData wave multipliers; Gravity Waves brush objects. | Live today (B2). Target: gone from flatspace. | Ruled. Forbidden: RadialWaves in the potential, the Gravity Waves objects on displacement layers. Pseudospace pulses (follow-up `pseudospace-flavours-campaign`) are a different kind, under the same grid invariant. | @@ -109,14 +113,15 @@ Cells not filled, plainly: whether a per-body multiplier on c or the core is eve - Why the calibration is on the sun's own field: field-strength-ruled names bodies at 12-18 u/s, which is v = sqrt(c) of the parent alone; the summed field ships feel is 2-6 times stronger (B21), which is the fork the question puts to the operator. - Why grid-lowering takes the Sector Waves noise off the grid now rather than leaving it until grid-noise-presentation: grid-gravity-only-ruled already decides that displacement carries gravity only, and the operator's flight on this branch (funnel-kept-until-flown) is to judge how the field moves her, which slopes that move nothing would blur. Keeping the noise drawn while the samplers read gravity only would need a second height texture (a camera or a compute target) that the next cut deletes. The noise stays in the scene on layer 10, drawn only into the nebula patch height, for grid-noise-presentation to place. - Why one field quad and not the per-body Gravity Well quads with the new law: B30. The per-body quads also cost a GameObject, a material instance and a scale per body, and a quad per body has to cover every camera's view because log tails have no support radius. +- Why a period is derived from `GravityField.Acceleration` rather than written as 2πd/sqrt(c): the formula is the tail law spelled a second time and is wrong inside the core (it would run a child faster than any free orbit there); T = 2π·sqrt(d/a) reads the one law, so the core, and any later change to the law, arrive in the rails with no edit (B35's 234 in-core children get a finite solid-body period). +- Why the asteroid periods live on the belt: Pose and BeltTargets must divide by the same float (BeltTargets.cs:62); one array computed at build is one owner, where two calls to the same function are two spellings that only agree by care. - A constraint for pseudospace, not solved here: a push with curl (B11) has no height, so under `grid-shows-how-you-move` it either never moves ships, or is drawn by another channel, or is restricted to its gradient part. ## Cut sequence -Mapped and admitted (2026-10-14, base 11b2d7ad): `aetheria-gravity:cut_spec:cut-gravity-subtract.r2` and its sibling `aetheria-gravity:cut_spec:cut-orbits-placed-at-build.r1`, both merged at 5dd1f6b7. Mapped and admitted (2026-10-14, base 5dd1f6b7): `aetheria-gravity:cut_spec:cut-potential-owner.r3` (rebased on 3f5cef68 on 2026-10-15, after rulings `funnel-kept-until-flown` and `core-inside-roche-curve`), one Hands pass; it carries follow-ups `deep-orbit-placement-test` and `gravity-subtract-test-residuals`. Between it and grid-lowering the drawn grid lags (B23). The operator will fly the funnel before ruling on it again (`funnel-kept-until-flown`), so potential-owner and grid-lowering must both be on one playable branch before that flight; grid-lowering is stacked on potential-owner, not a sibling (B26), and its map is the next Imagination pass once potential-owner's report lands. Mapped and admitted (2026-10-15, base dd06f39d, stacked on potential-owner's head): `aetheria-gravity:cut_spec:cut-grid-lowering.r1`, one Hands pass, Starfire-verified; on its branch the drawn grid is the field (B30) and the Sector Waves noise is off the grid until grid-noise-presentation. Mapped and admitted (2026-10-15, base 8f491fd3, stacked on grid-lowering's head): `aetheria-gravity:cut_spec:cut-fog-precision-and-jitter.r1`, the slope jitter (B32) and the patch's second gravity lowering (B33), one Hands pass. Not yet mapped: +Mapped and admitted (2026-10-14, base 11b2d7ad): `aetheria-gravity:cut_spec:cut-gravity-subtract.r2` and its sibling `aetheria-gravity:cut_spec:cut-orbits-placed-at-build.r1`, both merged at 5dd1f6b7. Mapped and admitted (2026-10-14, base 5dd1f6b7): `aetheria-gravity:cut_spec:cut-potential-owner.r3` (rebased on 3f5cef68 on 2026-10-15, after rulings `funnel-kept-until-flown` and `core-inside-roche-curve`), one Hands pass; it carries follow-ups `deep-orbit-placement-test` and `gravity-subtract-test-residuals`. Between it and grid-lowering the drawn grid lags (B23). The operator will fly the funnel before ruling on it again (`funnel-kept-until-flown`), so potential-owner and grid-lowering must both be on one playable branch before that flight; grid-lowering is stacked on potential-owner, not a sibling (B26), and its map is the next Imagination pass once potential-owner's report lands. Mapped and admitted (2026-10-15, base dd06f39d, stacked on potential-owner's head): `aetheria-gravity:cut_spec:cut-grid-lowering.r1`, one Hands pass, Starfire-verified; on its branch the drawn grid is the field (B30) and the Sector Waves noise is off the grid until grid-noise-presentation. Mapped and admitted (2026-10-15, base 8f491fd3, stacked on grid-lowering's head): `aetheria-gravity:cut_spec:cut-fog-precision-and-jitter.r1`, the slope jitter (B32) and the patch's second gravity lowering (B33), one Hands pass. Mapped and admitted (2026-10-15, base 8f491fd3, a sibling of fog-precision-and-jitter on grid-lowering's head, disjoint files): `aetheria-gravity:cut_spec:cut-orbits-from-field.r1`, periods from the field (B35-B38), one Hands pass; it raises `aetheria-gravity:question:rails-enclosed-or-parent` and is written for the recommended parent-only option. Not yet mapped: - **potential-owner** (mapped, above): one ServerShared potential (c from an authored compressed curve G·m^p, authored core-radius curve, uniform-disc core, log tail, superposition, analytic gradient, no gate, global K in PlanetSettings, reference zero at the root's zone radius); every CPU force reader (Ship, floating bodies, clouds, mines) and `Zone.Flow`'s slope term read it; GetHeight = φ/K; deletes `GravityAcceleration`, `GravityReferenceRate`, `GetNormal` as a force source, the PowerPulse well and `GravityDepthExponent`; field strength per field-strength-ruled. Waits on both admitted cuts. May split into the potential and its readers if one Hands pass cannot hold both. -- **orbits-from-field**: Orbit.Period = 2πd/sqrt(c_parent) for the orbit constructor, asteroid poses (Zone.cs:822) and BeltTargets.cs:63; `OrbitPeriod` deleted. Waits on potential-owner. -- **children-within-reach**: ZoneGenerator places children inside a fraction of the parent's tidal radius and the core radius inside the innermost child (softening-radius-ruled); tested over generated zones. It reads `Planet.RocheLimit` (lower bound) and half the tidal radius (upper bound); the windows per class are in B25, and a body with no window (4-7% of small bodies) gets no children. Waits on orbits-from-field. +- **children-within-reach**: ZoneGenerator places children inside a fraction of the parent's tidal radius and the core radius inside the innermost child (softening-radius-ruled); tested over generated zones. It reads `Planet.RocheLimit` (lower bound) and half the tidal radius (upper bound); the windows per class are in B25, and a body with no window (4-7% of small bodies) gets no children. Seam: it places children on GeneratorPlanets before any Zone exists, so it needs the strength a child's rail obeys (`Zone.PeriodAround`'s pull: parent well, or the ring law for bodyless parents) over generator data; it lifts that pull to a pure function both read, rather than spelling the ring law a second time, and its tidal radius uses the same pull. Waits on orbits-from-field. - **grid-noise-presentation**: the Sector Waves noise (after grid-lowering on layer 10, drawn only into the nebula patch height) and fine-grid detail move to non-slope presentation (grid-gravity-only-ruled); it also owns the Main Menu's own Surface Displacement brushes (B29, follow-up `menu-grid-brushes`). Waits on grid-lowering. - **top-speed-field**: top speed becomes a hull field; no drag below it, drag on the excess above it; the VelocityLimit clamp (Behaviors/VelocityLimit.cs) and always-on hull drag (Ship.cs:127-129) go; AI pilots brake actively; open: drag shape above the cap, consumables as modifiers, whether loot and wreckage come to rest (aetheria-gravity:ruling:no-drag-below-top-speed, target invariant orbits-do-not-decay). Waits on potential-owner and orbits-from-field.