From aefd0c0a924863db05d72f6cf8da79a7379fc2b8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 19:31:03 +0200 Subject: [PATCH 01/10] Zone places every orbit when it is built; delete AddOrbit and CreateOrbit Orbit.Position and PreviousPosition are set from GetOrbitPosition at the pack's time in the constructor, so wells, lights and poses are valid before the first Update. AddOrbit and CreateOrbit had no caller. Tests not yet run. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Zone.cs | 31 +--- .../ZonePlacedAtBuildTests.cs | 150 ++++++++++++++++++ 2 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs diff --git a/Assets/Scripts/ServerShared/Zone.cs b/Assets/Scripts/ServerShared/Zone.cs index 16373f147..715d965bd 100644 --- a/Assets/Scripts/ServerShared/Zone.cs +++ b/Assets/Scripts/ServerShared/Zone.cs @@ -161,6 +161,12 @@ public class Zone Orbits.Add(orbit.Key, new Orbit(Settings, cache.Get(orbit))); } + // Aetheria gravity (target invariant zone-valid-before-tick): every orbit is placed at the pack's time + // when the zone is built, so each well, light and body pose read before the first Update is where the + // body is. Velocity stays zero until the first Update differences two placed positions. + foreach (var orbit in Orbits) + orbit.Value.PreviousPosition = GetOrbitPosition(orbit.Key); + foreach (var body in pack.Planets) { var planet = cache.Get(body); @@ -266,11 +272,6 @@ public class Zone private bool IsExpired(ChunkWear wear) => wear.Damage <= 0f && (!wear.BrokenUntil.HasValue || wear.BrokenUntil.Value <= _time); - public void AddOrbit(OrbitData orbit) - { - Orbits.Add(_itemManager.ItemData.RefOf(orbit).Key, new Orbit(Settings, orbit)); - } - public void Update(float deltaTime) { _time += deltaTime; @@ -591,26 +592,6 @@ public class Zone into[i] = beltInstance.Pose(i, _time, parentPosition, Settings, ChunkRadius(new ChunkId(belt, i))); } - public OrbitData CreateOrbit(CultRecordKey parent, float2 position) - { - var parentPosition = GetOrbitPosition(parent); - var delta = position - parentPosition; - var distance = length(delta); - var period = Settings.OrbitPeriod.Evaluate(distance); - var phase = atan2(delta.y, delta.x) / (PI * 2); - var currentPhase = frac(_time / period); - var storedPhase = (float) frac(phase - currentPhase); - - var orbit = new OrbitData - { - Distance = distance, - Parent = new CultRecordRef(parent), - Phase = storedPhase - }; - Orbits.Add(_itemManager.ItemData.Upsert(orbit).Key, new Orbit(Settings, orbit)); - return orbit; - } - public SecurityLevel GetSecurityLevel(float2 pos) { if (GalaxyZone.Owner==null) return SecurityLevel.Open; diff --git a/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs new file mode 100644 index 000000000..74cdc2003 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.IO; +using CultMath; +using GameCult.Caching; +using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; + +// Aetheria gravity, target invariant zone-valid-before-tick: a zone's orbits are placed when it is built, so +// its wells, lights and body poses are right before the first Update (follow-up +// aetheria-release:follow_up:zone-orbits-unplaced-before-first-tick). The zone here is the production +// constructor reading a pack; nothing places an orbit but that constructor and Zone.Update. +public sealed class ZonePlacedAtBuildTests : IDisposable +{ + private static readonly float2 RootAt = new float2(300f, -200f); + private const float Distance = 400f; + private const float Period = 40f; + + private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-zoneplaced-" + Guid.NewGuid().ToString("N")); + private readonly List _caches = new List(); + + public ZonePlacedAtBuildTests() => Directory.CreateDirectory(_root); + + public void Dispose() + { + foreach (var cache in _caches) cache.Dispose(); + Directory.Delete(_root, true); + } + + private static GameplaySettings GameSettings() => new GameplaySettings + { + DefaultEntitySettings = new EntitySettings(), + Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } }, + QualityPriceModifier = new ExponentialLerp() + }; + + private static ExponentialCurve Flat(float v) => new ExponentialCurve { Constant = v }; + + // Flat space (no zone depth) with one planet whose well reaches 500 u, a fixed root orbit at RootAt and the + // planet orbiting it at Distance from `phase`. Every orbit period is Period seconds. + private static PlanetSettings Settings() => new PlanetSettings + { + OrbitPeriod = Flat(Period), + BodyRadius = Flat(20f), + GravityRadius = Flat(500f), + GravityDepth = Flat(100f), + GravityStrength = 1f, + WaveDepth = Flat(0f), WaveRadius = Flat(0f), WaveSpeed = Flat(0f), WaveFrequency = Flat(0f), + LightRadius = Flat(0f) + }; + + private sealed class Scene + { + public CultCache Cache; + public ItemManager Items; + public HullData Hull; + public Zone Zone; + public CultRecordKey PlanetOrbit; + } + + private Scene Build(float phase, double time) + { + var cache = AetheriaStores.Open(Path.Combine(_root, $"{_caches.Count}.cc"), catalogWritable: true); + _caches.Add(cache); + cache.Upsert(new VerseGrammar { Revision = 1 }); + var shape = new Shape(5, 5); + foreach (var cell in shape.AllCoordinates) shape[cell] = true; + var hull = new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = shape, Durability = 10, Mass = 1000 }; + cache.Upsert(hull); + var items = new ItemManager(cache, new ProvenanceLedger(), GameSettings(), _ => { }); + + var rootOrbit = cache.Upsert(new OrbitData { FixedPosition = RootAt }).Key; + var planetOrbit = cache.Upsert(new OrbitData + { + Parent = new CultRecordRef(rootOrbit), Distance = Distance, Phase = phase + }).Key; + var planet = cache.Upsert(new PlanetData { Orbit = new CultRecordRef(planetOrbit), Mass = 1f }).Key; + var pack = new ZonePack + { + Orbits = { new CultRecordRef(rootOrbit), new CultRecordRef(planetOrbit) }, + Planets = { new CultRecordRef(planet) }, + Radius = 5000f, + Mass = 10000f, + Time = time + }; + var zone = new Zone(items, Settings(), pack, new GalaxyZone { Name = "Placed", Owner = null }, null); + return new Scene { Cache = cache, Items = items, Hull = hull, Zone = zone, PlanetOrbit = planetOrbit }; + } + + [Theory] + [InlineData(.1f, 0.0)] + [InlineData(.35f, 0.0)] + [InlineData(.1f, 123.0)] + [InlineData(.35f, 123.0)] + public void ABodysWellSitsOnItsOrbitBeforeTheFirstTick(float phase, double time) + { + var s = Build(phase, time); + var orbit = s.Zone.Orbits[s.PlanetOrbit]; + var turns = (float) frac(time / orbit.Period); + var expected = RootAt + OrbitData.Evaluate(frac(turns + phase)) * Distance; + + Assert.InRange(length(orbit.Position - expected), 0f, .01f); + Assert.InRange(length(orbit.PreviousPosition - expected), 0f, .01f); + + // The well pulls toward the body's place: a point 30 u east of it is pushed west. + var force = s.Zone.GetForce(expected + new float2(30f, 0f)); + Assert.True(length(force) > 0f, "the well exerts a force"); + Assert.InRange(normalize(force).x, -1f, -.99f); + } + + [Fact] + public void TheFirstTickVelocityIsTheOrbitsSpeed() + { + var s = Build(.35f, 0.0); + const float dt = 1f / 60f; + s.Zone.Update(dt); + + var speed = length(s.Zone.Orbits[s.PlanetOrbit].Velocity); + var expected = 2f * PI * Distance / Period; + Assert.InRange(speed, expected * .99f, expected * 1.01f); + } + + [Fact] + public void AShipSteppedAloneFeelsTheBodyWhereItIs() + { + var s = Build(.1f, 0.0); + var planetAt = s.Zone.Orbits[s.PlanetOrbit].Position; + var ship = new Ship(s.Items, s.Zone, new EquippableItem + { + Data = s.Cache.RefOf(s.Hull), + Durability = s.Hull.Durability, + Lot = s.Items.Lots.Add(new Lot + { + Design = s.Cache.RefOf(s.Hull), Origin = new Attributed(), Quality = .5f, Roles = new List() + }) + }, new EntitySettings()); + s.Zone.Admit(ship, piloted: false); + var at = planetAt + new float2(0f, 60f); + ship.Position = float3(at.x, ship.Position.y, at.y); + ship.Velocity = float2.zero; + + ship.Update(1f / 60f); // the ship alone: no Zone.Update has run + + Assert.True(length(ship.Velocity) > 0f, "the ship is pulled"); + var toward = normalize(planetAt - at); + Assert.True(dot(normalize(ship.Velocity), toward) > .99f, "the pull is toward the planet, not toward the origin"); + } +} From 7d3fb29ed2a3e31613bb1b9fa4f7a3b152dabec9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 19:34:20 +0200 Subject: [PATCH 02/10] Gravity subtract: delete the zone bowl and the gas-giant and sun ripples GetHeight sums body wells only. Removes RadialWaves, GasGiant wave fields, GasGiantData keys 14-17, PlanetSettings Zone*/Wave* fields, the minimap gravity range, the Boundary brush and Gravity Waves objects from the scene and both prefabs. The five bowl fixtures become one WellFixture well at the origin with unchanged expected numbers. Not yet built or tested: this commit has not been through the Yggdrasil job. Co-Authored-By: Claude Opus 5.5 --- Assets/Prefabs/RPG/Planets/Gas Giant.prefab | 85 ------------- Assets/Prefabs/RPG/Planets/Sun.prefab | 85 ------------- Assets/Resources/Settings.asset | 4 +- Assets/Scenes/ARPG.unity | 92 -------------- Assets/Scripts/Gameplay/GameSettings.cs | 1 - Assets/Scripts/ServerShared/Settings.cs | 7 -- Assets/Scripts/ServerShared/Zone.cs | 32 +---- Assets/Scripts/ServerShared/ZoneData.cs | 12 -- Assets/Scripts/Zone Display/GasGiantObject.cs | 1 - Assets/Scripts/Zone Display/ZoneRenderer.cs | 18 +-- .../Aetheria.Shared.Tests/FireControlTests.cs | 19 +-- .../FloatingBodyTests.cs | 6 +- tests/Aetheria.Shared.Tests/FlowFieldTests.cs | 10 +- .../GravitySubtractTests.cs | 117 ++++++++++++++++++ tests/Aetheria.Shared.Tests/LootTests.cs | 14 ++- tests/Aetheria.Shared.Tests/MineTests.cs | 6 +- .../Aetheria.Shared.Tests/MiningCut3Tests.cs | 4 - tests/Aetheria.Shared.Tests/WellFixture.cs | 21 ++++ 18 files changed, 180 insertions(+), 354 deletions(-) create mode 100644 tests/Aetheria.Shared.Tests/GravitySubtractTests.cs create mode 100644 tests/Aetheria.Shared.Tests/WellFixture.cs diff --git a/Assets/Prefabs/RPG/Planets/Gas Giant.prefab b/Assets/Prefabs/RPG/Planets/Gas Giant.prefab index 46ae7a3b1..cb628d804 100644 --- a/Assets/Prefabs/RPG/Planets/Gas Giant.prefab +++ b/Assets/Prefabs/RPG/Planets/Gas Giant.prefab @@ -31,7 +31,6 @@ Transform: m_Children: - {fileID: 3973935122748960680} - {fileID: 3973935121058605868} - - {fileID: 3973935122114073483} - {fileID: 3973935121358855036} m_Father: {fileID: 0} m_RootOrder: 0 @@ -53,7 +52,6 @@ MonoBehaviour: Icon: {fileID: 3973935121358855037} Generator: {fileID: 0} SunMaterial: {fileID: 3973935122748960685} - GravityWaves: {fileID: 3973935122114073485} --- !u!1 &3973935121058605867 GameObject: m_ObjectHideFlags: 0 @@ -220,89 +218,6 @@ MeshRenderer: m_SortingLayer: 0 m_SortingOrder: 0 m_AdditionalVertexStreams: {fileID: 0} ---- !u!1 &3973935122114073482 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 3973935122114073483} - - component: {fileID: 3973935122114073486} - - component: {fileID: 3973935122114073485} - m_Layer: 10 - m_Name: Gravity Waves - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &3973935122114073483 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3973935122114073482} - m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 3973935120991099173} - m_RootOrder: 2 - m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} ---- !u!33 &3973935122114073486 -MeshFilter: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3973935122114073482} - m_Mesh: {fileID: 10210, guid: 0000000000000000e000000000000000, type: 0} ---- !u!23 &3973935122114073485 -MeshRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 3973935122114073482} - m_Enabled: 1 - m_CastShadows: 1 - m_ReceiveShadows: 1 - m_DynamicOccludee: 1 - m_StaticShadowCaster: 0 - m_MotionVectors: 1 - m_LightProbeUsage: 1 - m_ReflectionProbeUsage: 1 - m_RayTracingMode: 2 - m_RayTraceProcedural: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 2100000, guid: 11fbaa87dcc7a3743a6bd3aa5c86e5ae, type: 2} - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 3 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &3973935122748960663 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/RPG/Planets/Sun.prefab b/Assets/Prefabs/RPG/Planets/Sun.prefab index a7950f448..cf7fb216f 100644 --- a/Assets/Prefabs/RPG/Planets/Sun.prefab +++ b/Assets/Prefabs/RPG/Planets/Sun.prefab @@ -31,7 +31,6 @@ Transform: m_Children: - {fileID: 7852221720971737233} - {fileID: 7852221720969441490} - - {fileID: 7852221720741322892} - {fileID: 7852221720816817928} - {fileID: 7852221720964551245} m_Father: {fileID: 0} @@ -54,92 +53,8 @@ MonoBehaviour: Icon: {fileID: 7852221720964551246} Generator: {fileID: 0} SunMaterial: {fileID: 7852221720971737234} - GravityWaves: {fileID: 7852221720741322894} FogTint: {fileID: 7852221720816817929} Light: {fileID: 8064377225639657765} ---- !u!1 &7852221720741322899 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 7852221720741322892} - - component: {fileID: 7852221720741322895} - - component: {fileID: 7852221720741322894} - m_Layer: 10 - m_Name: Gravity Waves - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &7852221720741322892 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 7852221720741322899} - m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 7852221720480299767} - m_RootOrder: 2 - m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} ---- !u!33 &7852221720741322895 -MeshFilter: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 7852221720741322899} - m_Mesh: {fileID: 10210, guid: 0000000000000000e000000000000000, type: 0} ---- !u!23 &7852221720741322894 -MeshRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 7852221720741322899} - m_Enabled: 1 - m_CastShadows: 1 - m_ReceiveShadows: 1 - m_DynamicOccludee: 1 - m_StaticShadowCaster: 0 - m_MotionVectors: 1 - m_LightProbeUsage: 1 - m_ReflectionProbeUsage: 1 - m_RayTracingMode: 2 - m_RayTraceProcedural: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 2100000, guid: 11fbaa87dcc7a3743a6bd3aa5c86e5ae, type: 2} - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 3 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_AdditionalVertexStreams: {fileID: 0} --- !u!1 &7852221720816817935 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Resources/Settings.asset b/Assets/Resources/Settings.asset index 032d8c898..64c4333d7 100644 --- a/Assets/Resources/Settings.asset +++ b/Assets/Resources/Settings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:667368f42385152984589abb3803aa88a51937286ac2450d890970710ec38e37 -size 13039 +oid sha256:319895214aaeea5d4dfe034c46edca055f62c6b4a9692788e60ce8a5827c4b78 +size 12634 diff --git a/Assets/Scenes/ARPG.unity b/Assets/Scenes/ARPG.unity index b93a7b909..57c793e09 100644 --- a/Assets/Scenes/ARPG.unity +++ b/Assets/Scenes/ARPG.unity @@ -16322,7 +16322,6 @@ MonoBehaviour: Settings: {fileID: 11400000, guid: cd2344e2001a37544aab0960c164d4ff, type: 2} ZoneRoot: {fileID: 2095699057} SectorBrushes: {fileID: 1037490739} - SectorBoundaryBrush: {fileID: 1476765550} SceneCamera: {fileID: 887677622} FogCameras: - {fileID: 559840917} @@ -16864,7 +16863,6 @@ Transform: m_LocalScale: {x: 3000, y: 3000, z: 3000} m_ConstrainProportionsScale: 0 m_Children: - - {fileID: 1476765548} - {fileID: 23420357} - {fileID: 1108843909} - {fileID: 1647230682} @@ -22639,96 +22637,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} ---- !u!1 &1476765547 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1476765548} - - component: {fileID: 1476765551} - - component: {fileID: 1476765550} - m_Layer: 9 - m_Name: Boundary - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &1476765548 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1476765547} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 1037490739} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!23 &1476765550 -MeshRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1476765547} - m_Enabled: 1 - m_CastShadows: 0 - m_ReceiveShadows: 0 - m_DynamicOccludee: 0 - m_StaticShadowCaster: 0 - m_MotionVectors: 2 - m_LightProbeUsage: 0 - m_ReflectionProbeUsage: 0 - m_RayTracingMode: 2 - m_RayTraceProcedural: 0 - m_RayTracingAccelStructBuildFlagsOverride: 0 - m_RayTracingAccelStructBuildFlags: 1 - m_SmallMeshCulling: 1 - m_ForceMeshLod: -1 - m_MeshLodSelectionBias: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 2100000, guid: 8f45fb3a0f420d5428f1528827d55fa2, type: 2} - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 3 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_MaskInteraction: 0 - m_AdditionalVertexStreams: {fileID: 0} ---- !u!33 &1476765551 -MeshFilter: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1476765547} - m_Mesh: {fileID: 10210, guid: 0000000000000000e000000000000000, type: 0} --- !u!1 &1485884351 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Gameplay/GameSettings.cs b/Assets/Scripts/Gameplay/GameSettings.cs index d2b3df1d8..65e134755 100644 --- a/Assets/Scripts/Gameplay/GameSettings.cs +++ b/Assets/Scripts/Gameplay/GameSettings.cs @@ -15,7 +15,6 @@ public class GameSettings : ScriptableObject public float HeatstrokePhasingFrequency = 5; public float WormholeDistanceRatio; public float DefaultViewDistance; - public float MinimapZoneGravityRange; public float[] MinimapZoomLevels; public int DefaultMinimapZoom; public ExponentialCurve IconSize; diff --git a/Assets/Scripts/ServerShared/Settings.cs b/Assets/Scripts/ServerShared/Settings.cs index 1a04dd2ae..ab422c7fa 100644 --- a/Assets/Scripts/ServerShared/Settings.cs +++ b/Assets/Scripts/ServerShared/Settings.cs @@ -13,15 +13,8 @@ using static CultMath.math; [Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject] public class PlanetSettings { - public float ZoneDepthExponent; - public float ZoneDepth; - public float ZoneBoundaryFog; public ExponentialCurve GravityDepth; public ExponentialCurve GravityRadius; - public ExponentialCurve WaveDepth; - public ExponentialCurve WaveRadius; - public ExponentialCurve WaveFrequency; - public ExponentialCurve WaveSpeed; public ExponentialCurve LightRadius; public ExponentialCurve BodyRadius; public float AsteroidVerticalOffset = -5f; diff --git a/Assets/Scripts/ServerShared/Zone.cs b/Assets/Scripts/ServerShared/Zone.cs index 16373f147..54a6d047e 100644 --- a/Assets/Scripts/ServerShared/Zone.cs +++ b/Assets/Scripts/ServerShared/Zone.cs @@ -630,7 +630,7 @@ public class Zone public float GetHeight(float2 position) { - var result = -PowerPulse(length(position)/(Pack.Radius*2), Settings.ZoneDepthExponent) * Settings.ZoneDepth; + var result = 0f; foreach (var body in PlanetInstances.Values) { var p = position - body.Orbit.Position; //GetOrbitPosition(body.BodyData.Orbit) @@ -641,18 +641,6 @@ public class Zone var depth = body.GravityWellDepth; result -= PowerPulse(sqrt(distSqr) / gravityRadius, body.BodyData.GravityDepthExponent) * depth; } - - if (body is GasGiant gas) - { - var waveRadius = gas.GravityWavesRadius; - if(distSqr < waveRadius*waveRadius) - { - var depth = gas.GravityWavesDepth; - var frequency = Settings.WaveFrequency.Evaluate(body.BodyData.Mass); - var speed = gas.GravityWavesSpeed; - result -= RadialWaves(sqrt(distSqr) / waveRadius, 8, 1.25f, frequency, (float) (_time * speed)) * depth; - } - } } return result; @@ -716,12 +704,6 @@ public class Zone return pow((x + 1) * (1 - x), exponent); } - public static float RadialWaves(float x, float maskExponent, float sineExponent, float frequency, float phase) - { - //x *= 2; - return PowerPulse(x, maskExponent) * cos(pow(x*2, sineExponent) * frequency + phase); - } - public float3 GetNormal(float2 pos, float step = .1f, float mul = 1) { float hL = GetHeight(new float2(pos.x - step, pos.y)) * mul; @@ -763,22 +745,10 @@ public class Planet public class GasGiant : Planet { public GasGiantData GasGiantData; - public float GravityWavesDepth; - public float GravityWavesRadius; - public float GravityWavesSpeed; public GasGiant(PlanetSettings settings, GasGiantData data, Orbit orbit) : base(settings, data, orbit) { GasGiantData = data; - CalculateProperties(); - } - - public new void CalculateProperties() - { - base.CalculateProperties(); - GravityWavesDepth = Settings.WaveDepth.Evaluate(BodyData.Mass) * GasGiantData.WaveDepthMultiplier; - GravityWavesRadius = Settings.WaveRadius.Evaluate(BodyData.Mass) * GasGiantData.WaveRadiusMultiplier; - GravityWavesSpeed = Settings.WaveSpeed.Evaluate(BodyData.Mass) * GasGiantData.WaveSpeedMultiplier; } } diff --git a/Assets/Scripts/ServerShared/ZoneData.cs b/Assets/Scripts/ServerShared/ZoneData.cs index 7dc02b878..70448ac40 100644 --- a/Assets/Scripts/ServerShared/ZoneData.cs +++ b/Assets/Scripts/ServerShared/ZoneData.cs @@ -186,18 +186,6 @@ public class GasGiantData : BodyData [JsonProperty("albedoRotationSpeed"), Key(13)] public float AlbedoRotationSpeed = 1; - [JsonProperty("gravRadiusMul")] [Key(14)] - public float WaveRadiusMultiplier = 1; - - [JsonProperty("depthMul")] [Key(15)] - public float WaveDepthMultiplier = 1; - - [JsonProperty("depthExp")] [Key(16)] - public float WaveDepthExponent = 8; - - [JsonProperty("depthExp")] [Key(17)] - public float WaveSpeedMultiplier = 8; - [JsonProperty("materialOverrides"), Key(18)] public List MaterialOverrides = new List(); diff --git a/Assets/Scripts/Zone Display/GasGiantObject.cs b/Assets/Scripts/Zone Display/GasGiantObject.cs index 81fb45a1d..927513648 100644 --- a/Assets/Scripts/Zone Display/GasGiantObject.cs +++ b/Assets/Scripts/Zone Display/GasGiantObject.cs @@ -9,5 +9,4 @@ using UnityEngine; public class GasGiantObject : PlanetObject { public SunMaterial SunMaterial; - public MeshRenderer GravityWaves; } diff --git a/Assets/Scripts/Zone Display/ZoneRenderer.cs b/Assets/Scripts/Zone Display/ZoneRenderer.cs index cccb97ebf..907134dbc 100644 --- a/Assets/Scripts/Zone Display/ZoneRenderer.cs +++ b/Assets/Scripts/Zone Display/ZoneRenderer.cs @@ -27,7 +27,6 @@ public class ZoneRenderer : MonoBehaviour public GameSettings Settings; public Transform ZoneRoot; public Transform SectorBrushes; - public MeshRenderer SectorBoundaryBrush; public CinemachineVirtualCamera SceneCamera; public Camera[] FogCameras; public Camera[] MinimapCameras; @@ -472,17 +471,12 @@ public class ZoneRenderer : MonoBehaviour else planet = Instantiate(GasGiant, ZoneRoot); var gas = (GasGiantObject) planet; - var gasGiant = Zone.PlanetInstances[key] as GasGiant; gas.Body.material.SetTexture("_ColorRamp", gasGiantData.Colors.ToGradient(!(planetData is SunData)).ToTexture()); // gasGiantData.AlbedoRotationSpeed.Subscribe(f => gas.SunMaterial.AlbedoRotationSpeed = f); // gasGiantData.FirstOffsetRotationSpeed.Subscribe(f => gas.SunMaterial.FirstOffsetRotationSpeed = f); // gasGiantData.SecondOffsetRotationSpeed.Subscribe(f => gas.SunMaterial.SecondOffsetRotationSpeed = f); // gasGiantData.FirstOffsetDomainRotationSpeed.Subscribe(f => gas.SunMaterial.FirstOffsetDomainRotationSpeed = f); // gasGiantData.SecondOffsetDomainRotationSpeed.Subscribe(f => gas.SunMaterial.SecondOffsetDomainRotationSpeed = f); - gas.GravityWaves.transform.localScale = gasGiant.GravityWavesRadius * Vector3.one; - gas.GravityWaves.material.SetFloat("_Depth", gasGiant.GravityWavesDepth); - gas.GravityWaves.material.SetFloat("_Frequency", Settings.PlanetSettings.WaveFrequency.Evaluate(planetData.Mass)); - //gas.WaveScroll.Speed = Properties.GravitySettings.WaveSpeed.Evaluate(f); } else { @@ -584,8 +578,6 @@ public class ZoneRenderer : MonoBehaviour planet.Value.Body.transform.localPosition = new Vector3(0, height + planetInstance.BodyRadius * 2, 0); if (planet.Value is GasGiantObject gasGiantObject) { - gasGiantObject.GravityWaves.material.SetFloat("_Phase", - Zone.Time * ((GasGiant) Zone.PlanetInstances[planet.Key]).GravityWavesSpeed); if (!(planet.Value is SunObject)) { var toParent = normalize(Zone.GetOrbitPosition(Zone.Orbits[planetInstance.BodyData.Orbit.Key].Data.Parent.Key) - p); @@ -618,19 +610,13 @@ public class ZoneRenderer : MonoBehaviour } //var fogPos = FogCameraParent.position; - SectorBoundaryBrush.material.SetFloat("_Power", Settings.PlanetSettings.ZoneDepthExponent); - SectorBoundaryBrush.material.SetFloat("_Depth", Settings.PlanetSettings.ZoneDepth + Settings.PlanetSettings.ZoneBoundaryFog); - var startDepth = Zone.PowerPulse(Settings.MinimapZoneGravityRange, Settings.PlanetSettings.ZoneDepthExponent) * - Settings.PlanetSettings.ZoneDepth; - var depthRange = Settings.PlanetSettings.ZoneDepth - startDepth + maxDepth; + var depthRange = Mathf.Max(maxDepth, 1f); foreach (var mat in MapGravityMaterials) { - mat.SetFloat("_StartDepth", startDepth); + mat.SetFloat("_StartDepth", 0f); mat.SetFloat("_DepthRange", depthRange); } - //Shader.SetGlobalFloat("_GridOffset", Settings.PlanetSettings.ZoneBoundaryFog); // var gravPos = MinimapGravityQuad.transform.position; - // gravPos.y = -Settings.PlanetSettings.ZoneDepth - _maxDepth; // MinimapGravityQuad.transform.position = gravPos; // MinimapTintQuad.transform.position = gravPos - Vector3.up*10; } diff --git a/tests/Aetheria.Shared.Tests/FireControlTests.cs b/tests/Aetheria.Shared.Tests/FireControlTests.cs index 24919fba9..0f95a2078 100644 --- a/tests/Aetheria.Shared.Tests/FireControlTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlTests.cs @@ -41,7 +41,11 @@ public sealed class FireControlTests : IDisposable // A hull with one weapon hardpoint per (rotation, firingArc) pair, laid out along the X axis of a 1xN // shape so each gets its own cell. FiringArc 0 on a hardpoint means "use GameplaySettings.FiringArc". private (ItemManager items, Ship ship, EquippedItem[] weapons) BuildShipWithHardpoints( - params (ItemRotation rotation, float firingArc)[] hardpoints) + params (ItemRotation rotation, float firingArc)[] hardpoints) => BuildShipWithHardpoints(false, hardpoints); + + // `well` digs the zone one well at the origin (WellFixture), so heights differ across the zone. + private (ItemManager items, Ship ship, EquippedItem[] weapons) BuildShipWithHardpoints( + bool well, (ItemRotation rotation, float firingArc)[] hardpoints) { var hullShape = new Shape(Math.Max(hardpoints.Length, 3), 3); foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true; @@ -84,7 +88,10 @@ public sealed class FireControlTests : IDisposable for (var i = 0; i < hardpoints.Length; i++) ledger.Lots[2 + i] = new Lot { Origin = new Attributed(), Quality = 1 }; // guns var items = new ItemManager(_openCache, ledger, TestSettings(), _ => { }); - var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); + var planets = new PlanetSettings(); + var pack = new ZonePack(); + if (well) WellFixture.Dig(_openCache, pack, planets, pack.Radius, 1000f, 1f); + var zone = new Zone(items, planets, pack, new GalaxyZone { Name = "Test Zone", Owner = null }, null); var hullRef = items.ItemData.RefOf(items.ItemData.GetByName("TestHull")); var hull = new EquippableItem { Data = hullRef, Durability = 1, Lot = 1 }; @@ -182,10 +189,10 @@ public sealed class FireControlTests : IDisposable } // A shooter with one in-range, in-arc gun, a detected target at `targetPosition`, and a Minion driving the shooter. - private (Ship shooter, Ship target, Zone zone) BuildCombatEncounter(float3 targetPosition, float projectileSpeed = 0f, bool twoTargetGuns = false) + private (Ship shooter, Ship target, Zone zone) BuildCombatEncounter(float3 targetPosition, float projectileSpeed = 0f, bool twoTargetGuns = false, bool well = false) { var mounts = twoTargetGuns ? new[] { (ItemRotation.None, 0f), (ItemRotation.None, 0f) } : new[] { (ItemRotation.None, 0f) }; - var (items, shooter, weapons) = BuildShipWithHardpoints(mounts); + var (items, shooter, weapons) = BuildShipWithHardpoints(well, mounts); var weaponData = (InstantWeaponData) weapons[0].Data.Behaviors.Single(b => b is InstantWeaponData); weaponData.Damage = new PerformanceStat { Min = 10, Max = 10 }; weaponData.Cooldown = new PerformanceStat { Min = 1, Max = 1 }; @@ -376,9 +383,7 @@ public sealed class FireControlTests : IDisposable [Fact] public void TheAIMeasuresTheTargetOnTheGroundPlane() { - var (shooter, target, zone) = BuildCombatEncounter(float3(0, 0, 600), twoTargetGuns: true); - zone.Settings.ZoneDepth = 1000f; - zone.Settings.ZoneDepthExponent = 1f; + var (shooter, target, zone) = BuildCombatEncounter(float3(0, 0, 600), twoTargetGuns: true, well: true); var strong = target.Equipment.Single(x => x.Data.Name == "Gun"); var weak = target.Equipment.Single(x => x.Data.Name == "WeakGun"); zone.Update(1f); // heights follow the zone's depth from the first update diff --git a/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs b/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs index 5b98ba649..b15e71fb0 100644 --- a/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs +++ b/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs @@ -230,8 +230,10 @@ public sealed class FloatingBodyTests : IDisposable QualityPriceModifier = new ExponentialLerp() }; var items = new ItemManager(cache, new ProvenanceLedger(), settings, _ => { }); - var planets = new PlanetSettings { ZoneDepth = 1000, ZoneDepthExponent = 1, GravityStrength = 1 }; - zone = new Zone(items, planets, new ZonePack { Radius = 1000 }, new GalaxyZone { Name = "Test Zone", Owner = null }, null); + var planets = new PlanetSettings { GravityStrength = 1 }; + var pack = new ZonePack { Radius = 1000 }; + WellFixture.Dig(cache, pack, planets, 1000, 1000, 1); + zone = new Zone(items, planets, pack, new GalaxyZone { Name = "Test Zone", Owner = null }, null); var design = cache.GetByName("Skiff"); var hull = new EquippableItem { diff --git a/tests/Aetheria.Shared.Tests/FlowFieldTests.cs b/tests/Aetheria.Shared.Tests/FlowFieldTests.cs index 2c08c3aa1..58519026e 100644 --- a/tests/Aetheria.Shared.Tests/FlowFieldTests.cs +++ b/tests/Aetheria.Shared.Tests/FlowFieldTests.cs @@ -46,9 +46,13 @@ public sealed class FlowFieldTests : IDisposable }; // A zone at `time` (the pack's clock, as a saved zone restores it), flat unless `depth` digs the zone's own well. - private Zone ZoneWith(FlowSettings flow, double time = 0, float depth = 0f) => new Zone(_items, - new PlanetSettings { Flow = flow, ZoneDepth = depth, ZoneDepthExponent = 2f }, - new ZonePack { Time = time }, new GalaxyZone { Name = "Flow Zone", Owner = null }, null); + private Zone ZoneWith(FlowSettings flow, double time = 0, float depth = 0f) + { + var planets = new PlanetSettings { Flow = flow }; + var pack = new ZonePack { Time = time }; + if (depth > 0f) WellFixture.Dig(_items.ItemData, pack, planets, pack.Radius, depth, 2f); + return new Zone(_items, planets, pack, new GalaxyZone { Name = "Flow Zone", Owner = null }, null); + } private static readonly float3[] Probes = { diff --git a/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs b/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs new file mode 100644 index 000000000..abbeeeed6 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs @@ -0,0 +1,117 @@ +using System; +using System.IO; +using CultMath; +using GameCult.Caching; +using Xunit; +using float2 = CultMath.float2; +using float3 = CultMath.float3; + +// flatspace-no-bowl-no-ripples (aetheria-gravity target r3): a zone's height is the sum of its bodies' wells and +// nothing else. No zone-wide bowl sized by the pack, and no gas-giant or sun ripple that varies with zone time. +public sealed class GravitySubtractTests : IDisposable +{ + private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-gravsub-" + Guid.NewGuid().ToString("N")); + private readonly CultCache _cache; + private readonly ItemManager _items; + + public GravitySubtractTests() + { + Directory.CreateDirectory(_root); + _cache = AetheriaStores.Open(Path.Combine(_root, "grav.cc"), catalogWritable: true); + _cache.Upsert(new VerseGrammar { Revision = 1 }); + _cache.FlushAsync().Wait(); + var settings = new GameplaySettings + { + DefaultEntitySettings = new EntitySettings(), + Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } }, + QualityPriceModifier = new ExponentialLerp(), + TargetDetectionInfoThreshold = .1f, TargetGearInfoThreshold = .8f, + FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 2f + }; + _items = new ItemManager(_cache, new ProvenanceLedger(), settings, _ => { }); + } + + public void Dispose() + { + _cache.Dispose(); + Directory.Delete(_root, true); + } + + // Curves for a body whose well, light and orbit all vary with mass, so a body that read a different + // mass or multiplier than its twin would show it. + private static PlanetSettings Planets() => new PlanetSettings + { + GravityStrength = 1, + BodyRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 3f }, + GravityRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 500f }, + GravityDepth = new ExponentialCurve { Exponent = .175f, Multiplier = 30f }, + LightRadius = new ExponentialCurve { Exponent = .25f, Multiplier = 300f }, + OrbitPeriod = new ExponentialCurve() + }; + + private Zone Bare(float packRadius) => new Zone(_items, Planets(), new ZonePack { Radius = packRadius }, + new GalaxyZone { Name = "Bare Zone", Owner = null }, null); + + [Fact] + public void AZoneWithNoBodiesIsFlat() + { + foreach (var packRadius in new[] { 1000f, 2500f }) + { + var zone = Bare(packRadius); + foreach (var r in new[] { 0f, 300f, 577f, 900f, 2000f }) + { + var at = new float2(r, 0); + Assert.Equal(0f, zone.GetHeight(at)); + Assert.Equal(0f, zone.GetForce(at).x); + Assert.Equal(0f, zone.GetForce(at).y); + var inPlane = new float2(0, r); + Assert.Equal(0f, zone.GetHeight(inPlane)); + Assert.Equal(0f, zone.GetForce(inPlane).x); + Assert.Equal(0f, zone.GetForce(inPlane).y); + } + } + } + + // One body of `data` at the origin of a zone whose only body it is. + private Zone WithBody(BodyData data) + { + var orbit = _cache.Upsert(new OrbitData { FixedPosition = float2.zero }).Key; + data.Orbit = new CultRecordRef(orbit); + data.Mass = 100f; + data.GravityRadiusMultiplier = 1.5f; + data.GravityDepthMultiplier = .7f; + data.GravityDepthExponent = 2f; + var body = _cache.Upsert(data).Key; + var pack = new ZonePack { Radius = 1000f }; + pack.Orbits.Add(new CultRecordRef(orbit)); + pack.Planets.Add(new CultRecordRef(body)); + return new Zone(_items, Planets(), pack, new GalaxyZone { Name = "Giant " + data.GetType().Name, Owner = null }, null); + } + + [Fact] + public void AGiantsWellIsAPlainWellAtEveryTime() + { + var plain = WithBody(new PlanetData()); + var giants = new[] { WithBody(new GasGiantData()), WithBody(new SunData()) }; + foreach (var time in new[] { 0f, 37.5f }) + { + // Zone time 0 is the zones as built; 37.5 is one Update of that length. + if (time > 0f) + { + plain.Update(time); + foreach (var giant in giants) giant.Update(time); + } + foreach (var r in new[] { 50f, 200f, 400f }) + { + var at = new float2(r, 0); + Assert.True(plain.GetHeight(at) < 0f, "the plain body digs a well here, so there is something to compare"); + foreach (var giant in giants) + { + Assert.Equal(plain.GetHeight(at), giant.GetHeight(at)); + Assert.Equal(plain.GetForce(at).x, giant.GetForce(at).x); + Assert.Equal(plain.GetForce(at).y, giant.GetForce(at).y); + } + } + } + } +} diff --git a/tests/Aetheria.Shared.Tests/LootTests.cs b/tests/Aetheria.Shared.Tests/LootTests.cs index 6b736fa23..bf2b85793 100644 --- a/tests/Aetheria.Shared.Tests/LootTests.cs +++ b/tests/Aetheria.Shared.Tests/LootTests.cs @@ -61,7 +61,7 @@ public sealed class LootTests : IDisposable } private Lab Build(string zoneName = "Loot", float probability = .5f, float lifetime = 30f, float velocity = 25f, - PlanetSettings planets = null, float spread = 0f) + bool well = false, float spread = 0f) { var settings = new GameplaySettings { @@ -108,7 +108,14 @@ public sealed class LootTests : IDisposable var ledger = new ProvenanceLedger(); for (var i = 1; i <= 9000; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 }; var items = new ItemManager(cache, ledger, settings, _ => { }); - var zone = new Zone(items, planets ?? new PlanetSettings(), new ZonePack { Radius = planets == null ? 0 : 1000 }, new GalaxyZone { Name = zoneName, Owner = null }, null); + var planets = new PlanetSettings(); + var pack = new ZonePack { Radius = well ? 1000 : 0 }; + if (well) + { + planets.GravityStrength = 1; + WellFixture.Dig(cache, pack, planets, 1000, 1000, 1); + } + var zone = new Zone(items, planets, pack, new GalaxyZone { Name = zoneName, Owner = null }, null); return new Lab { Items = items, Zone = zone }; } @@ -286,8 +293,7 @@ public sealed class LootTests : IDisposable [Fact] public void AFloatingItemDriftsAsABodyAndExpires() { - var planets = new PlanetSettings { ZoneDepth = 1000, ZoneDepthExponent = 1, GravityStrength = 1 }; - var lab = Build(lifetime: 3f, planets: planets); + var lab = Build(lifetime: 3f, well: true); var settings = lab.Items.GameplaySettings; var start = new float2(40, -25); var launch = new float2(6, 2); diff --git a/tests/Aetheria.Shared.Tests/MineTests.cs b/tests/Aetheria.Shared.Tests/MineTests.cs index c997041a4..4155e80cf 100644 --- a/tests/Aetheria.Shared.Tests/MineTests.cs +++ b/tests/Aetheria.Shared.Tests/MineTests.cs @@ -118,8 +118,10 @@ public sealed class MineTests : IDisposable var ledger = new ProvenanceLedger(); for (var i = 1; i <= 500; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 }; var items = new ItemManager(cache, ledger, settings, _ => { }); - var planets = well ? new PlanetSettings { ZoneDepth = 1000, ZoneDepthExponent = 1, GravityStrength = 1 } : new PlanetSettings(); - var zone = new Zone(items, planets, new ZonePack { Radius = well ? 1000 : 0 }, new GalaxyZone { Name = "Mines", Owner = null }, null); + var planets = well ? new PlanetSettings { GravityStrength = 1 } : new PlanetSettings(); + var pack = new ZonePack { Radius = well ? 1000 : 0 }; + if (well) WellFixture.Dig(cache, pack, planets, 1000, 1000, 1); + var zone = new Zone(items, planets, pack, new GalaxyZone { Name = "Mines", Owner = null }, null); var lab = new Lab { Items = items, Zone = zone, Data = data, ItemData = cache.GetByName("Mine Layer") }; var layerRef = items.ItemData.RefOf(items.ItemData.GetByName("Layer")); diff --git a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs index 8ad9d8fc3..273b63547 100644 --- a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs +++ b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs @@ -58,10 +58,6 @@ public sealed partial class MiningCut3Tests : IDisposable BodyRadius = new ExponentialCurve(), GravityRadius = new ExponentialCurve(), GravityDepth = new ExponentialCurve(), - WaveDepth = new ExponentialCurve(), - WaveRadius = new ExponentialCurve(), - WaveSpeed = new ExponentialCurve(), - WaveFrequency = new ExponentialCurve(), LightRadius = new ExponentialCurve { Constant = lightRadius } }; diff --git a/tests/Aetheria.Shared.Tests/WellFixture.cs b/tests/Aetheria.Shared.Tests/WellFixture.cs new file mode 100644 index 000000000..ab6922cd7 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/WellFixture.cs @@ -0,0 +1,21 @@ +using CultMath; +using GameCult.Caching; + +// The one fixture that gives a test zone a slope: a single body on a parentless orbit at the origin, with +// the curves a body needs. Gravity height is the sum of body wells, so a test that wants ground to slide on +// digs it here and nowhere else. `radius` is the zone radius the well is sized to (its well reaches 2 x radius, +// as the former bowl did); the well is deepest at the centre and zero at and beyond half its radius. +internal static class WellFixture +{ + public static void Dig(CultCache cache, ZonePack pack, PlanetSettings settings, float radius, float depth, float exponent) + { + settings.BodyRadius = new ExponentialCurve(); + settings.OrbitPeriod = new ExponentialCurve(); + settings.GravityRadius = new ExponentialCurve { Constant = 2f * radius }; + settings.GravityDepth = new ExponentialCurve { Constant = depth }; + var orbit = cache.Upsert(new OrbitData { FixedPosition = float2.zero }).Key; + var body = cache.Upsert(new PlanetData { Orbit = new CultRecordRef(orbit), GravityDepthExponent = exponent }).Key; + pack.Orbits.Add(new CultRecordRef(orbit)); + pack.Planets.Add(new CultRecordRef(body)); + } +} From a8876d7a335375469bfafe9f3b44163fa61da17c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 19:34:21 +0200 Subject: [PATCH 03/10] ZonePlacedAtBuildTests: open the catalog and run stores as the other zone fixtures do Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs index 74cdc2003..87085b922 100644 --- a/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs +++ b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs @@ -62,7 +62,9 @@ public sealed class ZonePlacedAtBuildTests : IDisposable private Scene Build(float phase, double time) { - var cache = AetheriaStores.Open(Path.Combine(_root, $"{_caches.Count}.cc"), catalogWritable: true); + var dir = Path.Combine(_root, $"zone{_caches.Count}"); + Directory.CreateDirectory(dir); + var cache = AetheriaStores.Open(Path.Combine(dir, "Aetheria.cc"), Path.Combine(dir, "run.cc"), catalogWritable: true); _caches.Add(cache); cache.Upsert(new VerseGrammar { Revision = 1 }); var shape = new Shape(5, 5); From 55d55667433586edded425465617a0e83e2bbde1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 19:47:32 +0200 Subject: [PATCH 04/10] Gravity subtract tests: open the fixtures' caches with a run store OrbitData and BodyData are run-store types; a cache opened with a catalog only has no home for the well body. The six fixtures that dig a well now open with a run path. The previous commit does not pass its tests. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/FireControlTests.cs | 2 +- tests/Aetheria.Shared.Tests/FloatingBodyTests.cs | 2 +- tests/Aetheria.Shared.Tests/FlowFieldTests.cs | 2 +- tests/Aetheria.Shared.Tests/GravitySubtractTests.cs | 2 +- tests/Aetheria.Shared.Tests/LootTests.cs | 2 +- tests/Aetheria.Shared.Tests/MineTests.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/FireControlTests.cs b/tests/Aetheria.Shared.Tests/FireControlTests.cs index 0f95a2078..d4d0ba50d 100644 --- a/tests/Aetheria.Shared.Tests/FireControlTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlTests.cs @@ -82,7 +82,7 @@ public sealed class FireControlTests : IDisposable buildCache.FlushAsync().Wait(); } - _openCache = AetheriaStores.Open(Catalog, catalogWritable: true); + _openCache = AetheriaStores.Open(Catalog, Path.Combine(_root, "Run.cc"), catalogWritable: true); var ledger = new ProvenanceLedger(); ledger.Lots[1] = new Lot { Origin = new Attributed(), Quality = 1 }; // hull for (var i = 0; i < hardpoints.Length; i++) diff --git a/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs b/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs index b15e71fb0..e4ee47cfe 100644 --- a/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs +++ b/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs @@ -217,7 +217,7 @@ public sealed class FloatingBodyTests : IDisposable // the sign or changing how delta enters the line fails here, not only the Zone function's pin above. private Ship ShipAt(float x, out Zone zone) { - var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true); + var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), Path.Combine(_root, Guid.NewGuid().ToString("N") + ".run.cc"), catalogWritable: true); _caches.Add(cache); var hullShape = new Shape(5, 5); foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true; diff --git a/tests/Aetheria.Shared.Tests/FlowFieldTests.cs b/tests/Aetheria.Shared.Tests/FlowFieldTests.cs index 58519026e..4924daa81 100644 --- a/tests/Aetheria.Shared.Tests/FlowFieldTests.cs +++ b/tests/Aetheria.Shared.Tests/FlowFieldTests.cs @@ -19,7 +19,7 @@ public sealed class FlowFieldTests : IDisposable public FlowFieldTests() { Directory.CreateDirectory(_root); - _cache = AetheriaStores.Open(Path.Combine(_root, "flow.cc"), catalogWritable: true); + _cache = AetheriaStores.Open(Path.Combine(_root, "flow.cc"), Path.Combine(_root, "flow-run.cc"), catalogWritable: true); _cache.Upsert(new VerseGrammar { Revision = 1 }); _cache.FlushAsync().Wait(); var settings = new GameplaySettings diff --git a/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs b/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs index abbeeeed6..a513aad59 100644 --- a/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs +++ b/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs @@ -17,7 +17,7 @@ public sealed class GravitySubtractTests : IDisposable public GravitySubtractTests() { Directory.CreateDirectory(_root); - _cache = AetheriaStores.Open(Path.Combine(_root, "grav.cc"), catalogWritable: true); + _cache = AetheriaStores.Open(Path.Combine(_root, "grav.cc"), Path.Combine(_root, "grav-run.cc"), catalogWritable: true); _cache.Upsert(new VerseGrammar { Revision = 1 }); _cache.FlushAsync().Wait(); var settings = new GameplaySettings diff --git a/tests/Aetheria.Shared.Tests/LootTests.cs b/tests/Aetheria.Shared.Tests/LootTests.cs index bf2b85793..5a5bfce7d 100644 --- a/tests/Aetheria.Shared.Tests/LootTests.cs +++ b/tests/Aetheria.Shared.Tests/LootTests.cs @@ -73,7 +73,7 @@ public sealed class LootTests : IDisposable UnaidedAccuracy = .05f, UnaidedTracking = 10f, UnaidedPrecision = 2f, AgentMinHitProbability = 0f, LootDropProbability = probability, LootDropVelocity = velocity, FloatingItemLifetime = lifetime }; - var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true); + var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), Path.Combine(_root, Guid.NewGuid().ToString("N") + ".run.cc"), catalogWritable: true); _caches.Add(cache); cache.Upsert(new VerseGrammar { Revision = 1 }); var hardpoint = new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() }; diff --git a/tests/Aetheria.Shared.Tests/MineTests.cs b/tests/Aetheria.Shared.Tests/MineTests.cs index 4155e80cf..4d42e0add 100644 --- a/tests/Aetheria.Shared.Tests/MineTests.cs +++ b/tests/Aetheria.Shared.Tests/MineTests.cs @@ -78,7 +78,7 @@ public sealed class MineTests : IDisposable FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 2f, UnaidedAccuracy = .05f, UnaidedTracking = 10f, UnaidedPrecision = 1000f, AgentMinHitProbability = 0f }; - var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true); + var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), Path.Combine(_root, Guid.NewGuid().ToString("N") + ".run.cc"), catalogWritable: true); _caches.Add(cache); cache.Upsert(new VerseGrammar { Revision = 1 }); HullData Hull(string name, Shape shape) => new HullData From 9fb5aad4541726554e00846c9ee7856cb8946332 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 19:50:08 +0200 Subject: [PATCH 05/10] EvSettle returns the ship to where it stood A ship stepped alone drifts under the zone's wells while it idles. Orbits are now placed at build, so those wells are where the bodies are and the drift is real; the scenarios (Crossing, FusedRounds) staged geometry the wrong wells happened to leave undisturbed. Settling is about stats and power, not place. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/EvasionTermTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs index 0b0688457..ffd99d16e 100644 --- a/tests/Aetheria.Shared.Tests/EvasionTermTests.cs +++ b/tests/Aetheria.Shared.Tests/EvasionTermTests.cs @@ -81,11 +81,15 @@ public sealed partial class RunStartTests // Idle long enough for stats and power to reach their idle state, then at rest, facing +y, // and, after three seconds of coasting, with nothing observed: the track forgets on its own. + // The ship is stepped alone, so the zone's wells pull it while it idles; settling is about stats and power, so + // it comes back to where it stood. private static void EvSettle(Ship ship) { + var stood = ship.Position.xz; ship.MovementDirection = float2(0, 0); EvFace(ship, float3(0, 0, 1)); for (var step = 0; step < 180; step++) ship.Update(EvDt); + ship.Position.xz = stood; ship.Velocity = float2(0, 0); ship.Direction = float2(0, 1); } From 36d2dd45f21c8efc8dba62b87949c6fe1989e299 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:30:28 +0200 Subject: [PATCH 06/10] Gravity subtract tests: pin the well fixture to the bowl it replaced The five bowl fixtures compare to the zone's own normal, so nothing pinned the shared well's depth or radius to the bowl (a depth-scaled fixture mutant survived). TheWellFixtureReproducesTheFormerBowl checks heights against the bowl's own definition. This commit's test has not run yet. Co-Authored-By: Claude Opus 5.5 --- .../GravitySubtractTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs b/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs index a513aad59..8f23c1f6e 100644 --- a/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs +++ b/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs @@ -72,6 +72,26 @@ public sealed class GravitySubtractTests : IDisposable } } + // The fixture the five bowl tests share reproduces the zone bowl it replaced (map B14): the bowl was + // -PowerPulse(r / (2 R), exponent) x depth, written out here from its definition, deepest at the centre and + // zero from r = R on, so a fixture whose radius or depth drifted from the bowl would change every pull it feeds. + [Fact] + public void TheWellFixtureReproducesTheFormerBowl() + { + foreach (var (radius, depth, exponent, at, expected) in new[] + { + (1000f, 1000f, 1f, 0f, -1000f), (1000f, 1000f, 1f, 400f, -840f), (1000f, 1000f, 1f, 1000f, 0f), + (2000f, 64f, 2f, 1000f, -36f), (2000f, 64f, 2f, 300f, -61.1524f) + }) + { + var planets = Planets(); + var pack = new ZonePack { Radius = radius }; + WellFixture.Dig(_cache, pack, planets, radius, depth, exponent); + var zone = new Zone(_items, planets, pack, new GalaxyZone { Name = "Bowl Zone", Owner = null }, null); + Assert.InRange(zone.GetHeight(new float2(at, 0)), expected - 1e-2f, expected + 1e-2f); + } + } + // One body of `data` at the origin of a zone whose only body it is. private Zone WithBody(BodyData data) { From 205098ef93bd0f7fa59112abe4772d88c68fbc79 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 21:19:09 +0200 Subject: [PATCH 07/10] Zone.Update takes every previous position before placing any orbit GetOrbitPosition places a parent as a side effect of placing its child, so with a child listed first the parent's previous position was already its new one and its Velocity came out zero. The constructor's unread PreviousPosition write is dropped; the first Update takes it from the placed Position. The velocity test now runs moon-first and planet-first packs over varying dt. Builds not verified yet (verify follows). Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Zone.cs | 7 ++-- .../ZonePlacedAtBuildTests.cs | 32 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Assets/Scripts/ServerShared/Zone.cs b/Assets/Scripts/ServerShared/Zone.cs index 715d965bd..eee1aa869 100644 --- a/Assets/Scripts/ServerShared/Zone.cs +++ b/Assets/Scripts/ServerShared/Zone.cs @@ -165,7 +165,7 @@ public class Zone // when the zone is built, so each well, light and body pose read before the first Update is where the // body is. Velocity stays zero until the first Update differences two placed positions. foreach (var orbit in Orbits) - orbit.Value.PreviousPosition = GetOrbitPosition(orbit.Key); + GetOrbitPosition(orbit.Key); foreach (var body in pack.Planets) { @@ -276,9 +276,12 @@ public class Zone { _time += deltaTime; _updatedOrbits.Clear(); + // Every previous position is taken before any orbit is placed: GetOrbitPosition places a parent as a + // side effect of placing its child, so differencing inside one loop would depend on dictionary order. + foreach (var orbit in Orbits) + orbit.Value.PreviousPosition = orbit.Value.Position; foreach (var orbit in Orbits) { - orbit.Value.PreviousPosition = orbit.Value.Position; orbit.Value.Position = GetOrbitPosition(orbit.Key); orbit.Value.Velocity = (orbit.Value.Position - orbit.Value.PreviousPosition) / deltaTime; } diff --git a/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs index 87085b922..de9015a57 100644 --- a/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs +++ b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs @@ -60,7 +60,7 @@ public sealed class ZonePlacedAtBuildTests : IDisposable public CultRecordKey PlanetOrbit; } - private Scene Build(float phase, double time) + private Scene Build(float phase, double time, bool moonFirst = false) { var dir = Path.Combine(_root, $"zone{_caches.Count}"); Directory.CreateDirectory(dir); @@ -78,15 +78,21 @@ public sealed class ZonePlacedAtBuildTests : IDisposable { Parent = new CultRecordRef(rootOrbit), Distance = Distance, Phase = phase }).Key; + var moonOrbit = cache.Upsert(new OrbitData + { + Parent = new CultRecordRef(planetOrbit), Distance = 100f + }).Key; var planet = cache.Upsert(new PlanetData { Orbit = new CultRecordRef(planetOrbit), Mass = 1f }).Key; var pack = new ZonePack { - Orbits = { new CultRecordRef(rootOrbit), new CultRecordRef(planetOrbit) }, Planets = { new CultRecordRef(planet) }, Radius = 5000f, Mass = 10000f, Time = time }; + // The same three orbits either way; only the order the pack lists them in differs. + var listed = moonFirst ? new[] { moonOrbit, planetOrbit, rootOrbit } : new[] { rootOrbit, planetOrbit, moonOrbit }; + foreach (var key in listed) pack.Orbits.Add(new CultRecordRef(key)); var zone = new Zone(items, Settings(), pack, new GalaxyZone { Name = "Placed", Owner = null }, null); return new Scene { Cache = cache, Items = items, Hull = hull, Zone = zone, PlanetOrbit = planetOrbit }; } @@ -104,7 +110,6 @@ public sealed class ZonePlacedAtBuildTests : IDisposable var expected = RootAt + OrbitData.Evaluate(frac(turns + phase)) * Distance; Assert.InRange(length(orbit.Position - expected), 0f, .01f); - Assert.InRange(length(orbit.PreviousPosition - expected), 0f, .01f); // The well pulls toward the body's place: a point 30 u east of it is pushed west. var force = s.Zone.GetForce(expected + new float2(30f, 0f)); @@ -112,16 +117,21 @@ public sealed class ZonePlacedAtBuildTests : IDisposable Assert.InRange(normalize(force).x, -1f, -.99f); } - [Fact] - public void TheFirstTickVelocityIsTheOrbitsSpeed() + // A parent's velocity is its own orbital velocity on the first tick and every tick after, whichever order + // the pack lists a moon and its planet in. dt differs per tick so a velocity taken from a fixed step fails. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TheFirstTickVelocityIsTheOrbitsSpeed(bool moonFirst) { - var s = Build(.35f, 0.0); - const float dt = 1f / 60f; - s.Zone.Update(dt); - - var speed = length(s.Zone.Orbits[s.PlanetOrbit].Velocity); + var s = Build(.35f, 0.0, moonFirst); var expected = 2f * PI * Distance / Period; - Assert.InRange(speed, expected * .99f, expected * 1.01f); + foreach (var dt in new[] { 1f / 60f, 1f / 30f, 1f / 60f }) + { + s.Zone.Update(dt); + var speed = length(s.Zone.Orbits[s.PlanetOrbit].Velocity); + Assert.InRange(speed, expected * .99f, expected * 1.01f); + } } [Fact] From 3b9d7e0770b7ce2fef5bd6a377d2bb0a583082b8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 22:07:22 +0200 Subject: [PATCH 08/10] ZonePlacedAtBuildTests: drop the Wave curves the merged settings no longer have Builds after the merge of orbits-placed-at-build; the merge commit itself does not build. Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs index de9015a57..002f0f2df 100644 --- a/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs +++ b/tests/Aetheria.Shared.Tests/ZonePlacedAtBuildTests.cs @@ -47,7 +47,6 @@ public sealed class ZonePlacedAtBuildTests : IDisposable GravityRadius = Flat(500f), GravityDepth = Flat(100f), GravityStrength = 1f, - WaveDepth = Flat(0f), WaveRadius = Flat(0f), WaveSpeed = Flat(0f), WaveFrequency = Flat(0f), LightRadius = Flat(0f) }; From 7ae77e41a7127b80dca9faef2d6b907f3a72d70f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 22:08:18 +0200 Subject: [PATCH 09/10] Scenes: drop the Sun prefab-instance overrides that targeted the deleted Gravity Waves object Co-Authored-By: Claude Opus 5.5 --- Assets/Scenes/Main Menu.unity | 10 ---------- Assets/Scenes/TestScene.unity | 30 ------------------------------ 2 files changed, 40 deletions(-) diff --git a/Assets/Scenes/Main Menu.unity b/Assets/Scenes/Main Menu.unity index a37cfffba..2d80e45b7 100644 --- a/Assets/Scenes/Main Menu.unity +++ b/Assets/Scenes/Main Menu.unity @@ -1061,16 +1061,6 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 7852221720741322892, guid: 046dde0f4600bce41ad33c476f99b309, - type: 3} - propertyPath: m_LocalScale.x - value: 512 - objectReference: {fileID: 0} - - target: {fileID: 7852221720741322892, guid: 046dde0f4600bce41ad33c476f99b309, - type: 3} - propertyPath: m_LocalScale.y - value: 512 - objectReference: {fileID: 0} - target: {fileID: 7852221720816817928, guid: 046dde0f4600bce41ad33c476f99b309, type: 3} propertyPath: m_LocalScale.x diff --git a/Assets/Scenes/TestScene.unity b/Assets/Scenes/TestScene.unity index 5288b6845..ffc3f3905 100644 --- a/Assets/Scenes/TestScene.unity +++ b/Assets/Scenes/TestScene.unity @@ -911,16 +911,6 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 7852221720741322892, guid: 046dde0f4600bce41ad33c476f99b309, - type: 3} - propertyPath: m_LocalScale.x - value: 40 - objectReference: {fileID: 0} - - target: {fileID: 7852221720741322892, guid: 046dde0f4600bce41ad33c476f99b309, - type: 3} - propertyPath: m_LocalScale.y - value: 40 - objectReference: {fileID: 0} - target: {fileID: 7852221720969441490, guid: 046dde0f4600bce41ad33c476f99b309, type: 3} propertyPath: m_LocalScale.x @@ -1778,16 +1768,6 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 7852221720741322892, guid: 046dde0f4600bce41ad33c476f99b309, - type: 3} - propertyPath: m_LocalScale.x - value: 40 - objectReference: {fileID: 0} - - target: {fileID: 7852221720741322892, guid: 046dde0f4600bce41ad33c476f99b309, - type: 3} - propertyPath: m_LocalScale.y - value: 40 - objectReference: {fileID: 0} - target: {fileID: 7852221720969441490, guid: 046dde0f4600bce41ad33c476f99b309, type: 3} propertyPath: m_LocalScale.x @@ -5297,16 +5277,6 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} - - target: {fileID: 7852221720741322892, guid: 046dde0f4600bce41ad33c476f99b309, - type: 3} - propertyPath: m_LocalScale.x - value: 40 - objectReference: {fileID: 0} - - target: {fileID: 7852221720741322892, guid: 046dde0f4600bce41ad33c476f99b309, - type: 3} - propertyPath: m_LocalScale.y - value: 40 - objectReference: {fileID: 0} - target: {fileID: 7852221720969441490, guid: 046dde0f4600bce41ad33c476f99b309, type: 3} propertyPath: m_LocalScale.x From 2aad0fceca21477a54b0f27bc56bff813266bf20 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 22:09:48 +0200 Subject: [PATCH 10/10] Gravity subtract tests: sweep the giant's well densely; pin gas-giant keys 14-17 as retired Co-Authored-By: Claude Opus 5.5 --- .../GravitySubtractTests.cs | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs b/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs index 8f23c1f6e..7f89cd0fd 100644 --- a/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs +++ b/tests/Aetheria.Shared.Tests/GravitySubtractTests.cs @@ -108,23 +108,32 @@ public sealed class GravitySubtractTests : IDisposable return new Zone(_items, Planets(), pack, new GalaxyZone { Name = "Giant " + data.GetType().Name, Owner = null }, null); } + // The wells are the whole of a giant's height at every point, not just at a few radii: a ripple confined to + // any annulus, or to a band derived from the body's radius, shows against the plain body somewhere in a sweep + // that runs from the centre to well past the well's support. [Fact] public void AGiantsWellIsAPlainWellAtEveryTime() { var plain = WithBody(new PlanetData()); var giants = new[] { WithBody(new GasGiantData()), WithBody(new SunData()) }; - foreach (var time in new[] { 0f, 37.5f }) + var radii = new System.Collections.Generic.List { 0.5f }; + for (var r = 1f; r <= 100f; r += 1f) radii.Add(r); + for (var r = 105f; r <= 2000f; r += 5f) radii.Add(r); + var angles = new[] { 0f, 0.7f, 2.1f, 4.0f }; + foreach (var time in new[] { 0f, 37.5f, 91.25f }) { - // Zone time 0 is the zones as built; 37.5 is one Update of that length. - if (time > 0f) + // Zone time 0 is the zones as built; each later time is one Update to that time. + var step = time - _clock; + if (step > 0f) { - plain.Update(time); - foreach (var giant in giants) giant.Update(time); + plain.Update(step); + foreach (var giant in giants) giant.Update(step); + _clock = time; } - foreach (var r in new[] { 50f, 200f, 400f }) + foreach (var r in radii) + foreach (var a in angles) { - var at = new float2(r, 0); - Assert.True(plain.GetHeight(at) < 0f, "the plain body digs a well here, so there is something to compare"); + var at = new float2(r * (float)Math.Cos(a), r * (float)Math.Sin(a)); foreach (var giant in giants) { Assert.Equal(plain.GetHeight(at), giant.GetHeight(at)); @@ -132,6 +141,24 @@ public sealed class GravitySubtractTests : IDisposable Assert.Equal(plain.GetForce(at).y, giant.GetForce(at).y); } } + Assert.True(plain.GetHeight(new float2(50f, 0)) < 0f, "the plain body digs a well here, so there is something to compare"); } } + + private float _clock; + + // CultCache has no primitive for reserving a removed MessagePack key; the wave keys of the gas giant (14-17) + // must not come back with a different meaning, because stored records may still carry them. + [Fact] + public void AGasGiantRecordNeverReusesTheRetiredWaveKeys() + { + var keys = new System.Collections.Generic.List(); + for (var t = typeof(GasGiantData); t != null; t = t.BaseType) + foreach (var m in t.GetMembers(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly)) + if (m.GetCustomAttributes(typeof(MessagePack.KeyAttribute), false) is MessagePack.KeyAttribute[] ks) + foreach (var k in ks) if (k.IntKey.HasValue) keys.Add(k.IntKey.Value); + Assert.NotEmpty(keys); + foreach (var retired in new[] { 14, 15, 16, 17 }) Assert.DoesNotContain(retired, keys); + } }