36 KiB
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 perPlanetInstancesentry (radiusGravityWellRadius, depthGravityWellDepth, exponentBodyData.GravityDepthExponent), and for everyGasGiant(suns included) aRadialWavesripple in zone time.GetNormal(:725-734) differences GetHeight at ±0.1;GetForce(:692-696) andShip.Update(Ship.cs:133-135) feed the normal's xz toGravityAcceleration(:684-689),S·R·(1/(1-|n|²)-1)gated off at|n|² <= .001. Wells readbody.Orbit.Position, which is float2.zero until the firstZone.Update(follow-upzone-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
c6519383neither is disabled:Settings.asset(the only asset carrying PlanetSettings) hasZoneDepth: 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 sceneBoundaryobject (PowerBrush, layer Displacement, active) is driven each frame byZoneRenderer(ZoneRenderer.cs:621-622) with ZoneDepth plus ZoneBoundaryFog (0), and theGravity Wavesobjects inGas Giant.prefabandSun.prefab(layer FineDisplacement, active) carry the ripple. Soflatspace-no-bowl-no-ripplesis 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. CameraSurface Displacement(ARPG.unity, culling layers 9 Displacement and 10 FineDisplacement) renders additive brush quads intoAssets/Resources/Fog Surface Displacement.renderTexture, whichVolumeSampling.cs:63publishes 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 16fixed in the material,_Depthset fromGravityWellDepth, quad scaled byGravityWellRadius(ZoneRenderer.cs:500-503)the well in GetHeight, but the exponent is the material's 16, not BodyData.GravityDepthExponent, and the shader's envelope addssaturate(abs(x))-.001,min(_Cutoff, …)and asmoothstep(1,.95)rim fade the sim lacksGravity Waves (Gas Giant, Sun) 10 Gravity Waves, RadialWaveBrush, _Power 8,_Cutoff .5the ripple in GetHeight Boundary (ARPG scene) 9 Sector Boundary, PowerBrush, _Powerand_Depthdriven by ZoneRendererthe 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 Wavesnone: 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.csbinds it asHeightmap). -
B4. Readers of the drawn height (GPU).
Volumetric.cginc:121-130(gradient of_NebulaSurfaceHeight, the shader's slopeFlow and surface normal;:174surface displacement),Asteroid.shader:31(displacement),Stardust.compute:134(gravHeight),VolumeRenderCustom.shader(property),Slime.compute(Heightmap), the minimap gravity materials (ZoneRenderer.MapGravityMaterials,_StartDepth/_DepthRangeat :623-628), and the editor verifierFlowFieldParityVerify.cs:162,261. Follow-upaetheria-fauna:follow_up:flow-slope-gradient-parityalready 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),Zonefloating 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:114andBehaviors/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.Updatebuilds 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), withOrbitPeriod= 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 atFixedPosition. Two commented-out period writers remain inZoneGenerator.cs:589, 650. -
B7. Body well parameters.
Planet.CalculateProperties(Zone.cs:755-760):BodyRadius,GravityWellRadius,GravityWellDepthfrom PlanetSettings curves times per-body multipliers;GasGiantadds wave depth, radius, speed. The generator never writes any multiplier orGravityDepthExponent(no writer in ServerShared).GasGiantData's wave fields reuse the JSON namesgravRadiusMul,depthMulanddepthExp(twice:WaveDepthExponentandWaveSpeedMultiplier) thatBodyDataalready 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.WaveDepthExponentis 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.Massstarts 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, andGlobalis a noise field: neither is a gradient, so no height can show them as slopes. No ship readsZone.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 inIgnoredExtraSlotswith a warning, not an error. So deletingGasGiantDatakeys 14-17 (the wave fields) loads old run caches with acontent_hash_driftwarning. The shipped catalogGameData/Aetheria.ccholds noaetheria.gasgiantdatadocuments (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). APlanetDataon a parentless orbit atFixedPositionzero, withGravityRadiusconstant 2·Pack.Radius,GravityDepthconstant ZoneDepth andGravityDepthExponent= 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 setBodyRadius,GravityRadius,GravityDepthandOrbitPeriod(null curves throw inPlanet.CalculatePropertiesand theOrbitconstructor). -
B15. Where the drawn bowl and ripples live in Unity YAML (scan
yamlgo.py).Gas Giant.prefab: GameObjectGravity Waveslines 223-240, Transform 241-255, MeshFilter 256-263, MeshRenderer 264-305, parent's child entry line 34,GasGiantObject.GravityWavesreference line 56.Sun.prefab: GameObject 60-77, Transform 78-92, MeshFilter 93-100, MeshRenderer 101-142, child entry 34, reference 57.ARPG.unity: GameObjectBoundary22642-22659, Transform 22660-22674, MeshRenderer 22675-22723, MeshFilter 22724-22731, child entry 16867 (parent transform 1037490739),ZoneRenderer.SectorBoundaryBrushreference 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.csopensAssets/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) andZone.CreateOrbit(:594-611, the run-time orbit that back-solves its phase fromOrbitPeriod) have no caller under Assets/Scripts or tests. Every live orbit comes fromZonePack.Orbitsin 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
11b2d7adits git blob is a 3-line LFS pointer; the smudged worktree file is the YAML read in B2.tools/context-pack.pyreads 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.pyin the Imagination scratchpadimagination-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). Questionaetheria-gravity:question:superposed-wells-funnelcarries 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 fromGravityWellRadius/Depthuntil 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.Slopenormalises 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 aPlanetSettings; only the ones with bodies evaluate its curves, so new curves default tonew 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; rulingcore-inside-roche-curve, G as B19 underfunnel-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².
-
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/Depthin Zone.cs, BodyData keys 6-7 in ZoneData.cs,GravityRadius/GravityDepthin 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 targetFog Surface Displacement.renderTextureis 2048² R32_SFloat (GraphicsFormat 49), so φ/K's range (to about -700) keeps full precision; brushes blendOne 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:62today) 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).
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), CoreRadius (3·m^0.25) and RocheLimit (7.32·m^0.25, the closest a child orbits; B25, core-inside-roche-curve) in Settings.asset. Owner of the per-body value: Planet.WellStrength, CoreRadius and RocheLimit, set at construction (core inside Roche is checked there), read later by orbits-from-field and children-within-reach; never authored per body. Demoted until grid-lowering: GravityRadius, GravityDepth, Planet.GravityWellRadius/Depth and the two BodyData multipliers feed the brushes only. 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<OrbitData>; 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 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), which is the defect this campaign kills. Target: lowered from the sim's potential (h = φ/K), whether by an analytic shader function fed body parameters (as FlowField is lowered) or by brushes whose shapes are generated from the same law; the one owner is the ServerShared potential and the shader is a lowering with a parity check (as FlowFieldParityVerify does for flow). Forbidden: material constants (_Power, _Cutoff, _Depth), smoothstep rim fades, the wormhole and station gravity wells, and any brush on a gravity layer with no sim term. Open: grid-presentation-noise (what becomes of the Sector Waves noise and the liquid-grid detail). |
| 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 per-body well multipliers survive (no writer today, no consumer asked); what record names an Ink pillar; whether the PlanetOutpost prefab's drawn well is ever instantiated; 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-movemakes 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).
- Why the core is 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.
- A constraint for pseudospace, not solved here: a push with curl (B11) has no height, so under
grid-shows-how-you-moveit 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. Until grid-noise-presentation also lands, that flight still shows the Sector Waves noise as slopes. 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; deletesGravityAcceleration,GravityReferenceRate,GetNormalas a force source, the PowerPulse well andGravityDepthExponent; 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;
OrbitPerioddeleted. 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. - grid-lowering (stacked on potential-owner, B26): the renderer draws φ/K from the sim's law (analytic or generated brushes) with a parity check; deletes the Gravity Well brush constants and the wormhole gravity brushes; gravity samplers (slopeFlow, stardust, slime, minimap) read gravity only; closes
aetheria-fauna:follow_up:flow-slope-gradient-parity. Starfire-verified. Waits on potential-owner. - grid-noise-presentation: the Sector Waves noise and fine-grid detail move to non-slope presentation (grid-gravity-only-ruled). 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.