Aetheria/docs/aetheria-gravity-map.md
2026-10-15 18:00:22 +02:00

56 KiB
Raw Blame History

Aetheria gravity: map

Target: aetheria-gravity:target:r4 (rationale in docs/aetheria-gravity-target.md). Rulings in force: aetheria-gravity:ruling:grid-shows-how-you-move; from other campaigns, aetheria-release:ruling:gravity-honest-wells-agreed, aetheria-fauna:ruling:ink-pillars-are-suns, aetheria-fauna:ruling:sim-knows-the-flow, aetheria-release:ruling:liquid-grid-from-sim-brushes. Prior art: docs/research/2026-10-14-gravity-prior-art.md. Orbit measurements: docs/research/2026-10-13-orbit-probe/report.md.

Body facts

All read at c6519383 in the detached worktree C:\aeth-wt\ag2-docs on 2026-10-14 unless marked. Scripts live in the Imagination scratchpad (imagination-ag2-gravity-0b/).

  • B1. Sim gravity today. Zone.GetHeight (Zone.cs:631-658) sums the zone bowl -PowerPulse(|p|/(2·Pack.Radius), ZoneDepthExponent)·ZoneDepth, one PowerPulse well per PlanetInstances entry (radius GravityWellRadius, depth GravityWellDepth, exponent BodyData.GravityDepthExponent), and for every GasGiant (suns included) a RadialWaves ripple in zone time. GetNormal (:725-734) differences GetHeight at ±0.1; GetForce (:692-696) and Ship.Update (Ship.cs:133-135) feed the normal's xz to GravityAcceleration (:684-689), S·R·(1/(1-|n|²)-1) gated off at |n|² <= .001. Wells read body.Orbit.Position, which is float2.zero until the first Zone.Update (follow-up zone-orbits-unplaced-before-first-tick).

  • B2. The bowl and the ripples are live in code. The operator's ruling says the zone bowl is disabled and stays so, ripples likewise. At c6519383 neither is disabled: Settings.asset (the only asset carrying PlanetSettings) has ZoneDepth: 64, ZoneDepthExponent: 2, WaveDepth 0.5·m^0.3; GetHeight applies both unconditionally; the bowl is felt only in zones with radius under about 3117 (the force gate, probe F3) and the ripples are felt around every gas giant and sun (8 to 32% of peak force, probe wave table). The drawn grid shows both: the scene Boundary object (PowerBrush, layer Displacement, active) is driven each frame by ZoneRenderer (ZoneRenderer.cs:621-622) with ZoneDepth plus ZoneBoundaryFog (0), and the Gravity Waves objects in Gas Giant.prefab and Sun.prefab (layer FineDisplacement, active) carry the ripple. So flatspace-no-bowl-no-ripples is delivered by deletion in a cut, not already true.

  • B3. The drawn grid is a second height owner. It is not sampled from Zone.GetHeight. Camera Surface Displacement (ARPG.unity, culling layers 9 Displacement and 10 FineDisplacement) renders additive brush quads into Assets/Resources/Fog Surface Displacement.renderTexture, which VolumeSampling.cs:63 publishes as the global _NebulaSurfaceHeight. Brushes on those layers:

    object (where) layer material and shader sim counterpart
    Gravity Well (Planet, Gas Giant, Sun prefabs) 9 Gravity Well, PowerBrush, _Power 16 fixed in the material, _Depth set from GravityWellDepth, quad scaled by GravityWellRadius (ZoneRenderer.cs:500-503) the well in GetHeight, but the exponent is the material's 16, not BodyData.GravityDepthExponent, and the shader's envelope adds saturate(abs(x))-.001, min(_Cutoff, …) and a smoothstep(1,.95) rim fade the sim lacks
    Gravity Waves (Gas Giant, Sun) 10 Gravity Waves, RadialWaveBrush, _Power 8, _Cutoff .5 the ripple in GetHeight
    Boundary (ARPG scene) 9 Sector Boundary, PowerBrush, _Power and _Depth driven by ZoneRenderer the zone bowl
    Waves Small / Medium / Large (ARPG scene) 10 Sector Waves *, SimplexBrush, depths -16.75, 7.58, 2 none: drawn, never felt
    Gravity Well, Gravity Pit, Gravity Waves (Wormhole 1 prefab) 9 Wormhole Gravity 1 (_Depth 54, _Power 4), Wormhole Gravity (_Depth 2000, _Power 64, _Cutoff .15), Wormhole Waves none: the sim has no wormhole well
    Gravity Well (PlanetOutpost station prefab) 9 Gravity Well none found; whether this prefab is instantiated in play was not checked

    Other cameras render the same layers into their own textures: Patch Displacement (layers 9, 10, 12), Map Gravity (9, the minimap; MapView.cs), Slime Gravity Renderer (9; Slime.cs binds it as Heightmap).

  • B4. Readers of the drawn height (GPU). Volumetric.cginc:121-130 (gradient of _NebulaSurfaceHeight, the shader's slopeFlow and surface normal; :174 surface displacement), Asteroid.shader:31 (displacement), Stardust.compute:134 (gravHeight), VolumeRenderCustom.shader (property), Slime.compute (Heightmap), the minimap gravity materials (ZoneRenderer.MapGravityMaterials, _StartDepth/_DepthRange at :623-628), and the editor verifier FlowFieldParityVerify.cs:162,261. Follow-up aetheria-fauna:follow_up:flow-slope-gradient-parity already names the slopeFlow mismatch.

  • B5. Readers of the sim height and force (CPU). Force: Ship.Update (Ship.cs:133-135, through GetNormal and GravityAcceleration directly, not GetForce), Zone floating bodies, clouds and mines (Zone.cs:411, 437, 451 via GetForce). Gradient for flow: Zone.Flow (:700-704). Height for placement: Entity.cs:1849 (Position.y of non-ship entities), Agents/States/Combat.cs:114 and Behaviors/TurretController.cs:73 (predicted target height), Gameplay/ItemPickup.cs:44, Gameplay/MineInstance.cs:59, ZoneRenderer.cs:545 (belt bounds) and :581 (planet body height, minimap depth range). Ship tilt: Ship.Update builds Rotation from the same normal. Tests: FloatingBodyTests.cs:187-209, 254 (pin the gate and the legacy law), LootTests.cs:299-308, MineTests.cs:623-628, FlowFieldTests.cs (zone with bowl as the slope source), FireControlTests.cs:380. Editor smoke: FloatingItemsDrawnSmoke.cs:159. No AI reader predicts gravity.

  • B6. Rails. Orbit.Period = OrbitPeriod.Evaluate(Distance) (Zone.cs:910), with OrbitPeriod = 1·d^1 + 0 in Settings.asset: period is a function of distance only, never of the parent. Other readers of the same curve: Zone.cs:599 (an orbit added at run time, phase back-solved), Zone.cs:822 (each asteroid of a belt), Targeting/BeltTargets.cs:63 (belt target periods). GetOrbitPosition (:465-500) is the closed-form rail; the root orbit has no parent and sits at FixedPosition. Two commented-out period writers remain in ZoneGenerator.cs:589, 650.

  • B7. Body well parameters. Planet.CalculateProperties (Zone.cs:755-760): BodyRadius, GravityWellRadius, GravityWellDepth from PlanetSettings curves times per-body multipliers; GasGiant adds wave depth, radius, speed. The generator never writes any multiplier or GravityDepthExponent (no writer in ServerShared). GasGiantData's wave fields reuse the JSON names gravRadiusMul, depthMul and depthExp (twice: WaveDepthExponent and WaveSpeedMultiplier) that BodyData already uses for its gravity fields; MessagePack keys differ (14-17 against 6-8), so the binary store is unambiguous and the JSON names are not. WaveDepthExponent is read by nothing (the sim hard-codes 8).

  • B8. Ship scale, measured headless from GameData/Aetheria.cc (CultCache Python runtime, hulls.py, mass.py). Hull Drag: Longinus 0.1/s, Smoke Test Hull 0.1, Djinni 0.2, stations and turret 0. Drag is linear: Velocity = normalize(v)·v·exp(-Drag·dt) (Ship.cs:128-129; CultMath.decay). Thrust ranges: Small Drive 60-90k, Medium 100-300k, Large 200-300k, Rugged 75-250k, Burn and High-Thrust 250k-1M, Racing 200k-1.5M. Entity.Mass starts at hull mass and changes as items are added and removed (Entity.cs:1251, 1407). Items weigh about 40-50 per schematic cell (median over gear, weapons, cargo bays); the Longinus schematic has 66 cells, the Djinni 166. So a Longinus with one Small Drive accelerates at 24-36 u/s² empty and about 11-16 u/s² fully packed (2500 + ~3000); a Djinni 6-9 empty, about 3.4-5 packed. "Fully packed" is an estimate from cell count times median density, not a generated loadout (LoadoutGenerator was not run). Agent TopSpeed defaults to 100 (VelocityLimit).

  • B9. What drag means for orbits. Holding a circular orbit at speed v costs a tangential thrust of Drag·v against drag, plus whatever radial thrust the field mismatch needs. At v = 6.3 (today's rail speed) that is 0.63 u/s² for a Longinus (2-6% of its range), 1.26 for a Djinni (14-37%); at v = 17.5 it is 1.75 (5-16%) and 3.5 (39-100%). No free orbit exists for any hull with Drag > 0; with honest wells a ship in orbit needs only the drag term, where today it also fights a radial mismatch of 17-30% of thrust (probe F2).

  • B10. Tide under superposed 1/r tails (computed, hill.py, over the probe's bodies). 2D tidal radius of a parent at R from its own parent: d_H = R·sqrt(c_p/(2c_g)). Fraction of the 17,864 children of non-root parents that orbit inside 0.5·d_H: 11% if c ∝ mass, 35% for m^0.5, 59% for m^0.25, 66% for m^0.175; children of planets 1%, 10%, 30%, 38%. Speeds: if a median sun's field is 0.5 u/s² at 610 u, v_circ = 17.5 u/s around it and 12.4 around a median planet (m^0.175).

  • B11. The flow has curl. FlowField.Slope (FlowField.cs:53-59) adds a swirl term, the downhill direction rotated 90 degrees, and Global is a noise field: neither is a gradient, so no height can show them as slopes. No ship reads Zone.Flow; it drives stardust and nebula (shader) and, by ruling, fauna.

  • B12. A ruling in force contradicts the target. aetheria-release:ruling:one-gravity-law (Defaulted, 2026-10-07) fixes the slope-squared law and "the feel ships have today". The target replaces both. Superseded by Self on 2026-10-14.

  • B13. Removing a persisted member is compatible drift. CultCache resolves a persisted schema against the local one slot by slot (CultLib/src/GameCult.Caching/CultCache.cs, CompareSchemaShapes, read 2026-10-14): a persisted slot the local type no longer has is listed in IgnoredExtraSlots with a warning, not an error. So deleting GasGiantData keys 14-17 (the wave fields) loads old run caches with a content_hash_drift warning. The shipped catalog GameData/Aetheria.cc holds no aetheria.gasgiantdata documents (B8's type listing); generated zone packs in run caches do.

  • B14. The zone bowl is exactly one well at the origin. Bowl: -PowerPulse(r/(2·Pack.Radius), ZoneDepthExponent)·ZoneDepth. Well: -PowerPulse(r/GravityWellRadius, GravityDepthExponent)·GravityWellDepth, zero at and beyond half the radius either way (PowerPulse doubles then clamps). A PlanetData on a parentless orbit at FixedPosition zero, with GravityRadius constant 2·Pack.Radius, GravityDepth constant ZoneDepth and GravityDepthExponent = ZoneDepthExponent, gives the same height and force at every point. The five test fixtures that dig a bowl to get a slope (FloatingBodyTests.cs:233, LootTests.cs:289, MineTests.cs:121, FlowFieldTests.cs:49-51, FireControlTests.cs:380-381) can therefore keep every expected number. Such a fixture must also set BodyRadius, GravityRadius, GravityDepth and OrbitPeriod (null curves throw in Planet.CalculateProperties and the Orbit constructor).

  • B15. Where the drawn bowl and ripples live in Unity YAML (scan yamlgo.py). Gas Giant.prefab: GameObject Gravity Waves lines 223-240, Transform 241-255, MeshFilter 256-263, MeshRenderer 264-305, parent's child entry line 34, GasGiantObject.GravityWaves reference line 56. Sun.prefab: GameObject 60-77, Transform 78-92, MeshFilter 93-100, MeshRenderer 101-142, child entry 34, reference 57. ARPG.unity: GameObject Boundary 22642-22659, Transform 22660-22674, MeshRenderer 22675-22723, MeshFilter 22724-22731, child entry 16867 (parent transform 1037490739), ZoneRenderer.SectorBoundaryBrush reference 16325. Neither prefab is a variant (no PrefabInstance documents).

  • B16. Unity-side code compiles only on Starfire. The Yggdrasil verify job builds and tests Aetheria.Shared.Tests (ServerShared); ZoneRenderer.cs, GasGiantObject.cs, GameSettings.cs, scenes and prefabs are compiled and loaded only by the Unity editor. Assets/Scripts/Editor/FloatingItemsDrawnSmoke.cs opens Assets/Scenes/ARPG.unity (line 238) in batchmode and exits nonzero on failure, so it is the cheapest check that the scene still loads and the renderer compiles.

  • B17. Dead orbit writers. Zone.AddOrbit (Zone.cs:269-272) and Zone.CreateOrbit (:594-611, the run-time orbit that back-solves its phase from OrbitPeriod) have no caller under Assets/Scripts or tests. Every live orbit comes from ZonePack.Orbits in the constructor (:157-160). GameSettings.MinimapZoneGravityRange (GameSettings.cs:18, Settings.asset line 23) feeds only the bowl's minimap start depth (ZoneRenderer.cs:623).

  • B18. Settings.asset is an LFS object. At 11b2d7ad its git blob is a 3-line LFS pointer; the smudged worktree file is the YAML read in B2. tools/context-pack.py reads base blobs, so a spec cannot line-anchor it (cut-gravity-subtract r1 was superseded by r2 for this); edits to it are named by key, and verify jobs copy the edited file in after LFS staging.

  • B19. The calibrated strength (computed, probe1.py in the Imagination scratchpad imagination-ag2-potential-owner/, over the orbit probe's 31,640 bodies at 5dd1f6b7's generator). Median sun mass 14152 (n 4187); median distance of a planet from its sun 609.8 (n 6112). field-strength-ruled gives c_sun = 0.5 x 609.8 = 304.9 u²/s² (v = 17.46 u/s). With c = G·m^p and p = 0.2 (the middle of the ruled 0.175-0.25): G = 45.08; c = 88.5 (median planetoid, 29.2), 138.5 (planet, 273.9), 211.3 (gas giant, 2260), 304.9 (sun); circular speeds 9.4, 11.8, 14.5, 17.5 u/s. p = 0.175 gives G 57.25, p = 0.25 gives G 27.96. A zone's bodies sum to c ≈ 2760 (median; p90 4210) and the root holds 18% of it (p10 13%).

  • B20. The core radius at the first default (superseded by B25 under core-inside-roche-curve; kept because it is why the core shrank). softening-radius-ruled defaulted the core to today's slope-peak radius: the peak slope of PowerPulse(x, 16) lies at x = 1/sqrt(31) of the half-support, so R = (500·m^0.25 / 2)/sqrt(31) = 44.9·m^0.25 (planetoid 104, planet 183, gas giant 310, sun 490). Peak acceleration c/R: 0.85, 0.76, 0.68, 0.62 u/s², so with p < 0.25 a small body's core is the steeper one. Innermost child (belt inner edges included) against 44.9·m^0.25 (probe2.py): inside the default core for 78% of suns' families, 67% of planets', 83% of gas giants', 73% of planetoids'; 9-21% of parents have a child inside 3·m^0.25 (the body radius). The bound "the core sits inside the innermost child" therefore cannot hold for today's generator under any useful core curve; it is enforced by children-within-reach, which places children. That cut also meets a window problem: a median planet at 610 u from a median sun has a tidal radius d_H = 610·sqrt(138.5/(2·304.9)) = 291, so half of it (145) is inside the planet's default core (183) and no moon can satisfy both rules until one curve moves.

  • B21. Summed tails make a funnel (probe3.py, probe4.py, probe6.py; random phases, 300 zones per radius). At p = 0.2 a ship at mid-zone feels 1.63 u/s² (zone radius 1000), 1.39 (2125), 1.02 (4182), 0.76 (6846), 0.59 (10000), about 90% of it toward the middle; at the zone edge 1.15 to 0.41. Beside a sun's planet the total field is 2.9x the sun's own 0.5 (median; p90 6.0). Alternatives probed at p = 0.175 (mid-zone inward pull 1.10): subtracting a uniform disc of the zone's total c over the zone radius, 0.69; capping each tail with c·L/r² beyond L = 100·m^0.25, 0.55 (and 300·m^0.25, 0.87); p = 0.5, 0.80; p = 1, 1.36 (the heaviest suns dominate). Question aetheria-gravity:question:superposed-wells-funnel carries the fork.

  • B22. Heights, reference and K (probe3.py). φ_i(r) = c·r²/(2R²) inside R, c·(ln(r/R) + 1/2) beyond; h = Σ(φ_i(r_i) - φ_i(Z))/K with Z = Pack.Radius, a constant per body, so heights never shift as bodies move and are finite everywhere (the core removes the log's singularity; nothing grows faster than ln r). At K = 5 a body's own well from Z = 4182 to its centre is 74 (planetoid), 101, 131, 161 (sun) deep, the depths today's brushes draw (54, 80, 116, 160); the steepest drawn slope of a lone well is c/(R·K) = 0.13-0.17. The summed field draws the zone as a cone: deepest point -264, -503, -745, -944, -1106 for zone radii 1000 to 10000, mid-zone -136 to -473, edge 0. K is one authored number; only heights and ship tilt read it in this cut. These numbers use the first core (44.9·m^0.25); B25 gives them for the shrunk core and K = 10.

  • B23. The drawn grid lags one cut. The potential cut changes GetHeight; the Gravity Well brushes still draw PowerPulse wells from GravityWellRadius/Depth until grid-lowering. So between the two cuts every placement reader (B5) puts ships, loot, mines and planet bodies at φ/K, up to hundreds of units below the drawn grid in the middle of a zone. Brush depths cannot copy φ/K: a quad per body cannot draw the summed tails.

  • B24. Readers at 5dd1f6b7. GravityStrength: Settings.cs:24, Settings.asset (2), Ship.cs:135, Zone.cs:667 and five test fixtures. GetNormal: Ship.cs:133 (force and tilt) and Zone.Flow (Zone.cs:674); FlowField.Slope normalises its input, so any vector along the downhill serves. GravityAcceleration: Ship, Zone.GetForce and FloatingBodyTests. GravityDepthExponent: ZoneData.cs:97, Zone.cs:626, WellFixture, GravitySubtractTests. Forty-eight test files build a PlanetSettings; only the ones with bodies evaluate its curves, so new curves default to new ExponentialCurve() (c = 0). Unity-side code reads none of the deleted names (no Editor or Zone Display reader), so the Starfire smoke is a load check for Settings.asset only.

  • B25. Roche limit, shrunk core and the moon window (probe7.py, probe3.py; ruling core-inside-roche-curve, G as B19 under funnel-kept-until-flown). Choice: RocheLimit = 7.32·m^0.25, which is the classical fluid Roche limit 2.44 x the body radius (BodyRadius = 3·m^0.25, equal densities); CoreRadius = 3·m^0.25, the body's own radius, so the well is a uniform body (force linear inside, peak at the surface) and core/Roche = 0.41 at every mass. Window for a body's children, [its Roche limit, half its tidal radius d_H = D·sqrt(c/(2c_parent))] (the root's upper bound is the zone radius):

    body class n window exists hi/lo p10, p50 window of the median body
    planetoid 10878 93% 1.12, 2.74 [19, 52]
    planet 8121 94% 1.21, 3.81 [27, 105]
    gas giant 3979 96% 1.46, 6.39 [41, 264]
    sun 4187 100% 5.37, 14.2 [71, 1000]

    A median planet at 610 u from a median sun: [29.8, 145]. Today's generated children already inside their parent's window: 20% (planetoids' children), 35%, 68%, 94% (suns'); children-within-reach moves the rest. Roche 5·m^0.25 would open 98-100% of windows, 10·m^0.25 only 83-99%. Per class (c, core, Roche, peak a = c/core, a at the Roche limit): planetoid 88.5, 7.0, 17.0, 12.7, 5.2; planet 138.5, 12.2, 29.8, 11.4, 4.7; gas giant 211.3, 20.7, 50.5, 10.2, 4.2; sun 304.9, 32.7, 79.8, 9.3, 3.8 u/s². A ship whose thrust is below c/r cannot climb out at r, whatever the core: a packed Djinni (about 4 u/s²) is held inside 76 u of a median sun, 35 u of a median planet. K = 10 draws each body's own well from Z = 4182 to its centre 61, 88, 123, 163 deep (today's brushes 54, 80, 116, 160); the steepest drawn slope, at a body's surface, is 0.93-1.27. Summed cone at K = 10: deepest -198, -342, -478, -588, -675 for zone radii 1000 to 10000, mid-zone -69 to -237; mid-zone pull 1.79 to 0.60 u/s².

  • B31. Core at the Roche limit, K = 4.098 (ruling core-at-roche-and-exaggeration-up, after the operator flew the stacked branch on 2026-10-15; landed in grid-lowering h4, 8f491fd3). Each well's core radius is its Roche limit (7.32·m^0.25); there is no separate core curve. K = 10 / (7.32/3) = 4.0983607, so the peak drawn slope c/(R·K) at the Roche radius equals the earlier c/(3·m^0.25 · 10), and every slope outside the cores draws 2.44x steeper than at K = 10 (pinned by ThePeakDrawnSlopeStaysAndTheFunnelDrawsSteeper; TheCoreRadiusIsTheRocheCurve locates the core). This supersedes the core and K numbers in B22, B25 and B28; depths scale by about 2.44 (median cone about -1160 deep), still within R32 precision. Children orbit outside the Roche limit, so they orbit outside the core. GravityFieldParityVerify on 8f491fd3: PASS, max error 0.0106 u, max gradient angle 0.03°.

  • B26. Grid-lowering is not a sibling of potential-owner (read at 3f5cef68). It reads what potential-owner adds (Planet.WellStrength, CoreRadius, HeightExaggeration, GravityField's law) and deletes what potential-owner deliberately keeps (Planet.GravityWellRadius/Depth in Zone.cs, BodyData keys 6-7 in ZoneData.cs, GravityRadius/GravityDepth in Settings.cs and in the LFS blob Settings.asset, which two branches cannot merge textually). So it is stacked: its branch is cut from potential-owner's head once that report lands. Facts for its map: the height target Fog Surface Displacement.renderTexture is 2048² R32_SFloat (GraphicsFormat 49), so φ/K's range (to about -700) keeps full precision; brushes blend One One, so wells drawn one quad per body add exactly as the potential superposes, and the four cameras that render layer 9 (Surface Displacement, Patch Displacement, Map Gravity, Slime Gravity Renderer, B3) would all receive the lowered potential with no change to their wiring if the Gravity Well material's shader becomes the one-well law (PowerBrush.shader:62 today) and each quad covers its camera's view rather than a support radius (log tails have none). That is a candidate, not a decision: the alternative is one pass over a body buffer, as FlowField is lowered (VolumeSampling.LowerFlow).

  • B27. The drawn height's cameras and readers at dd06f39d (scan p2.py, p4.py in the Imagination scratchpad imagination-ag2-grid-lowering/, 2026-10-15). Only five ARPG.unity cameras render layer 9 or 10, so a renderer on layer 9 reaches exactly these:

    camera (fileID) layers target, size, format window texel readers
    Surface Displacement (1537342003, the GridCamera and a FogCamera) 9, 10 (m_Bits: 1536, ARPG.unity:23476) Fog Surface Displacement, 2048², R32_SFloat follows the view, orthographic size = ViewDistance (4096 default) 4 u _NebulaSurfaceHeight: Volumetric.cginc gravGradient (slopeFlow, nebula surface normal), density (nebula floor), Stardust.compute:134 gravHeight, Asteroid.shader:31, and the drawn grid itself (Grid 1.mat, Tessellation Shader: _DisplacementMap = this texture, _DisplacementStrength -1, vertex y += -texel)
    Patch Displacement (507065483, a FogCamera) 9, 10, 12 Fog Patch Displacement, 256², R16_SFloat ViewDistance 32 u _NebulaPatchHeight (nebula patch density only); half precision is 1 u per step at the deepest h (B28), presentation only
    Slime Gravity Renderer (1066823975) 9 Slime Gravity, 1024², R32_SFloat zone, orthographic size = Pack.Radius 2R/1024 (8 u at R 4182) Slime.compute Heightmap (agents' height and normal)
    Map Gravity (326516967, 488792362) 9 MinimapGravity 128², R32_SFloat; MapView swaps in a half-screen RFloat minimap distance (250-2000) 4-31 u minimap materials, normalised by _DepthRange = the deepest body centre's -GetHeight (ZoneRenderer.cs:571-617, already from h, not from brush depths)

    Every texture holds depth = -h (brushes write positive _Depth; Stardust and the grid negate it). Layer 10 now holds only the three Sector Waves noise quads (SimplexBrush); gravity-subtract removed the giants' Gravity Waves. Layer 9 holds the Gravity Well child of Planet, Gas Giant and Sun prefabs (scaled by GravityWellRadius, _Depth = GravityWellDepth, ZoneRenderer.cs:494-498) and Wormhole 1's Gravity Well, Gravity Waves and Gravity Pit. The main scene camera culls neither layer.

  • B28. Bodies, depth and precision at the branch's curves (p1.py, p3.py over docs/research/2026-10-13-orbit-probe/data/bodies.csv; c = 45.08·m^0.2, core 3·m^0.25, K = 10, reference at Pack.Radius, random phases). Bodies per zone (1500 zones): p10 8, p50 17, p90 28, p99 57, max 80. Over 200 zones (4000 random points plus 20 per body inside three core radii): float32 against float64 of h = Σ(φ_i(r_i) - φ_i(Z))/K differs by at most 9e-4 u (p50 1.9e-4); deepest h p50 -476, minimum -1865 (a sun's centre in a large zone; B25's -675 is the median zone's cone). R32's step at 1865 is 1.2e-4 u, so the height target loses nothing to precision; resolution limits it instead (a planetoid's 7 u core is under two Surface Displacement texels). The texture's central difference over two 4 u texels points within 1.0° (median zone) and 2.9° (worst zone) of GetForce at the 99.9th percentile, away from 16 u of any body's centre.

  • B29. Dead gravity brushes and leftovers. Assets/Content/Prefabs/Stations/PlanetOutpost.prefab (a Gravity Well child, lines 84-164, on PlanetObject.GravityWell) is referenced by no GUID in the repo and its name is in no script or in GameData/Aetheria.cc, so it is never instantiated; Assets/Prefabs/RTS/Planet.prefab (a Gravity Well child bound to an RTS script's GravityMesh, not on layer 9) is likewise unreferenced. Materials no live object uses once the wormhole children go: Wormhole Gravity 1, Wormhole Gravity 2 (already unused), Wormhole Gravity, Wormhole Waves, and Gravity Waves (unused since gravity-subtract); WormholeBrush.shader and RadialWaveBrush.shader have no other material. PowerBrush.shader stays: thirteen other materials use it. ZoneRenderer.WormholeInstances' gravity member is the whole wormhole instance, not its brush. Main Menu.unity has its own Surface Displacement (9, 10) with a Boundary bowl on layer 9 and no zone.

  • B30. How the renderer lowers the law: one pass over a body buffer, not a brush per body (decided for grid-lowering from B27-B28). Candidates: (a) the Gravity Well quad per body with the one-well law, each covering every camera's view, blended One One; (b) a CPU-baked texture; (c) one quad on layer 9 whose fragment sums every well from a buffer the renderer fills each frame from Zone.PlanetInstances. (b) cannot follow moving bodies at 2048² and four windows. (a) and (c) evaluate the same N wells per texel (Surface Displacement: 4.2M texels x N, N = 17 median, 80 max), but (a) also reads and writes the R32 target once per body: 34 MB per body per frame on Surface Displacement alone, 0.57 GB at the median zone and 2.7 GB at the largest, where (c) writes it once. (c) sums in the sim's order (each well minus its reference, then divided by K) from the sim's own per-body reference, so the parity error is B28's float32 floor plus the GPU's log; and the four cameras need no rewiring, because they already cull layer 9. It follows the flow's precedent: the law lives in ServerShared, a shader include lowers it, and a batchmode verify renders it and compares with the C# (FlowFieldParityVerify). Body count sets the cost, so the buffer is a StructuredBuffer (no cap to truncate at), which needs shader target 4.5; the project already runs compute (Stardust, Slime).

  • 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

One row per kind this campaign touches. "Today" is c6519383; "target" is the shape under r2 with the recommended answers, marked where a question is open.

kind identity lifecycle authority (one owner per decision; forbidden writers)
Zone potential φ(p, t) One per Zone instance, unnamed (a method, not a record). Not persisted: derived from the pack's bodies, orbits and PlanetSettings at zone time. Today it does not exist as such; GetHeight is a shaped heightfield (mass-neutral wells, probe Reading B). Built when the zone is constructed; must be valid before the first Update (zone-valid-before-tick). Varies with body positions over zone time. No ripple term in flatspace (flatspace-no-bowl-no-ripples). Reference: each body's term is zero at distance Pack.Radius from it, a per-body constant, so heights never shift as bodies move (B22). Summed tails draw a zone-wide cone (B21; question superposed-wells-funnel). Owner (cut potential-owner): the one-well law in GravityField (ServerShared, pure static beside FlowField, so shaders can lower it), summed over PlanetInstances by Zone.GetForce and Zone.GetHeight. Forbidden writers: GravityAcceleration's slope-squared law and its gate, GetNormal as a force source, the zone bowl term, RadialWaves, any brush or shader that composes gravity on its own.
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<BodyData>), 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 pack record keyed by CultRecordKey, parent by CultRecordRef<OrbitData>; 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.
Exaggeration factor K PlanetSettings.HeightExaggeration (cut potential-owner), 10 to start (B25); h = φ/K. Authored; constant in play. Ruled one global value (exaggeration-scope-ruled). Owner: PlanetSettings. Forbidden: per-class or per-material depth scales, ZoneRenderer's _StartDepth/_DepthRange as an independent mapping of depth (it may normalise the minimap, but from h, not from brush depths).
Drawn grid height (_NebulaSurfaceHeight and the other displacement targets) Render textures named in B3; global shader names. Rendered every frame from brush quads. Today: the brushes own it, independently of the sim (B3). Target (cut grid-lowering, B30): one Gravity Field quad on layer 9 whose shader (Gravity.cginc) sums the sim's wells from a buffer GravityLowering fills each frame from Zone.PlanetInstances and Zone.WellReference; the owner stays the ServerShared potential, and GravityFieldParityVerify renders the quad and compares every texel with -GetHeight. Every camera that draws a gravity texture culls layer 9 only (Surface Displacement drops layer 10), so the drawn grid, slopeFlow, stardust, slime and the minimap read gravity only. Forbidden: material constants (_Power, _Cutoff, _Depth), smoothstep rim fades, per-body brush quads, the wormhole and station gravity wells, and any brush on a gravity layer with no sim term. The Sector Waves noise is off the grid from that cut until grid-noise-presentation gives it a non-slope channel (grid-gravity-only-ruled).
Readers that place things at the height (B5) Call sites. Every frame. Readers only. They read h = φ/K + GridOffset. No change of owner; listed so a cut finds them all.
Flow (FlowField) PlanetSettings Flow; Zone.Flow; shader globalFlow/slopeFlow. Zone time. Owner: FlowField in ServerShared (sim-knows-the-flow). Its slope term must read the potential's gradient, not GetNormal of a separate heightfield. Open: flow-moves-ships (B11).
Ship motion under gravity Ship.Update. Per tick: drag, then position, then gravity. Reader of the force. Hull Drag owns damping (B8, B9). Forbidden: any gravity computation local to Ship.

Cells not filled, plainly: whether a per-body multiplier on c or the core is ever wanted (none exists; the brush multipliers go in grid-lowering); what record names an Ink pillar; whether the zone needs a non-gravitational edge once the bowl is gone; real generated loadout masses (B8 is a bound, not a measurement).

Rationale

  • Why the drawn grid is a lowering, not an owner: grid-shows-how-you-move makes the slope a promise about motion. Six brush kinds on the gravity layers have no sim term or a different envelope (B3), so today the grid shows motion that does not happen (sector noise, wormholes, rim fades) and hides motion that does (the bowl in small zones is drawn at the same depth everywhere while felt only under the gate).
  • Why rails follow the field: every shipped game where ships feel gravity derives the rails from the same parameter (KSP) or simulates bodies (Outer Wilds); authored rails coexist only with no felt gravity (Elite, Starsector). See the prior-art doc, section 3.
  • Why strength, softening and tide are separate questions: a·r = v² ties felt strength to body speed (field-strength); c(mass) sets both readability and how many moons a ship can follow (well-strength-from-mass, B10); R sets the peak (softening-radius). Any two can be ruled without the third.
  • Why the force gate is not in the subtraction cut: deleting the gate changes forces everywhere the gate zeroed them (every core, every rim), so it is behaviour, not subtraction, and the function it lives in (GravityAcceleration) is deleted whole by the potential cut. Removing it first would rewrite FloatingBodyTests' gate assertions into a no-gate test of a law that dies one cut later.
  • Why the bowl fixtures become a well and not a new test helper per file: B14 makes the substitution exact, so the subtraction cut changes no expected number in five behaviour tests; the potential cut then replaces the fixture's well law once, in one helper.
  • Why orbits are placed at build in a sibling: it touches the zone constructor, Update and the dead run-time orbit writers (B17), none of which the subtraction cut touches, and it removes the stacked-wells-at-origin state before the potential cut needs a valid field at construction.
  • Why the potential cut is not split into the potential and its readers: the readers are three lines (Ship's force and tilt, Zone.Flow's downhill); floating bodies, clouds and mines already call GetForce. Deleting GravityAcceleration forces Ship's change in the same commit, so a split would leave a cut that cannot build.
  • Why the reference is per body at the zone radius, not a zone-wide constant: a constant per body never changes as bodies move, so the height of a fixed point changes only because wells moved, never because the reference did; and the edge of the zone sits near zero whatever the body count (B22).
  • (Superseded by B31: the core is now the Roche limit.) Why the core was the body's radius and the Roche limit 2.44 body radii: both are mass curves with the body radius's exponent, so core < Roche holds at every mass by construction rather than by a check over the generator's range; 2.44 is the classical fluid Roche coefficient at equal densities, and it opens a moon window for 93-100% of bodies (B25). A smaller multiplier opens more windows but lets moons skim the surface.
  • 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. 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.
  • 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.