diff --git a/Assets/Scripts/Editor/DrawnPoseWiringVerify.cs b/Assets/Scripts/Editor/DrawnPoseWiringVerify.cs index 8a170dbde..c1994ebeb 100644 --- a/Assets/Scripts/Editor/DrawnPoseWiringVerify.cs +++ b/Assets/Scripts/Editor/DrawnPoseWiringVerify.cs @@ -10,8 +10,8 @@ using float2 = CultMath.float2; using float3 = CultMath.float3; // Batch proof that the presenters read the drawn pose through the clock: DrawnPoseTests pins the pose maths headlessly, -// and this is what pins that each presenter passes ActionGameManager.Clock.Lead into it. A zone is loaded into a fresh -// ZoneRenderer with the pickup and mine prefabs ARPG.unity's ZoneRenderer names; a loose Projectile is launched on the +// and this is what pins that each presenter passes ActionGameManager.Clock.Lead into it. A zone with a sun and a planet on a moving rail +// is built and the pickup and mine prefabs ARPG.unity's ZoneRenderer names are bound to it as the renderer binds them; a loose Projectile is launched on the // zone. The zone is stepped once, then the clock is set to leads 0, 0.008 and 0.015 s with no step, and each presenter's // own Update places its transform. A body is drawn at its last step plus its step velocity times the lead, spelled here // from the body's fields, so a presenter that reads lead 0 (or half the lead) sits on the step and fails. @@ -110,32 +110,34 @@ namespace Aetheria.EditorTools var oneCell = new Shape(1, 1); oneCell[new CultMath.int2(0, 0)] = true; - var cache = AetheriaStores.Open(Path.Combine(_work, "wiring.cc"), catalogWritable: true); + var cache = AetheriaStores.Open(Path.Combine(_work, "wiring.cc"), Path.Combine(_work, "wiring-run.cc"), catalogWritable: true); cache.Upsert(new VerseGrammar { Revision = 1 }); cache.Upsert(new SimpleCommodityData { Name = "Wiring Ore", Shape = oneCell, MaxStack = 100 }); cache.FlushAsync().Wait(); var ledger = new ProvenanceLedger(); var items = new ItemManager(cache, ledger, settings, _ => { }); - var zone = new Zone(items, new PlanetSettings(), new ZonePack(), null, null); + + // A sun and a planet on a moving rail round it, both near the bodies below: the field under a floater changes with the + // drawn time, so a presenter that samples it at the step's time, drops it, or reads lead 0 for the height draws a different y. + var sunOrbit = cache.Upsert(new OrbitData { FixedPosition = new float2(-55, -.5f) }).Key; + var planetOrbit = cache.Upsert(new OrbitData { Parent = new CultRecordRef(sunOrbit), Distance = 40f, Phase = 0f }).Key; + var pack = new ZonePack { Radius = 5000f, Mass = 10000f }; + pack.Orbits.Add(new CultRecordRef(sunOrbit)); + pack.Orbits.Add(new CultRecordRef(planetOrbit)); + pack.Planets.Add(new CultRecordRef(cache.Upsert(new PlanetData { Name = "Wiring Sun", Orbit = new CultRecordRef(sunOrbit), Mass = 400f }).Key)); + pack.Planets.Add(new CultRecordRef(cache.Upsert(new PlanetData { Name = "Wiring Planet", Orbit = new CultRecordRef(planetOrbit), Mass = 400f }).Key)); + var curves = new PlanetSettings + { + BodyRadius = new ExponentialCurve(), + WellStrength = new ExponentialCurve { Multiplier = 1f, Exponent = 1f }, + RocheLimit = new ExponentialCurve { Constant = 10f } + }; + var zone = new Zone(items, curves, pack, null, null); var rootObject = new GameObject("Wiring Zone Root"); - var rendererObject = new GameObject("Wiring Zone Renderer"); - rendererObject.SetActive(false); // inactive: ZoneRenderer.Update needs a GPU and is not part of this check - var renderer = rendererObject.AddComponent(); - renderer.ZoneRoot = rootObject.transform; - renderer.SimpleCommodityPickup = pickupPrefab; - renderer.MinePrefab = minePrefab; - renderer.ItemManager = items; - renderer.SectorBrushes = new GameObject("Wiring Brushes").transform; - renderer.AmbientLight = AmbientEdgeVerify.NewAmbientLight(); - renderer.SlimeGravityCamera = new GameObject("Wiring Gravity Camera").AddComponent(); - var slimeObject = new GameObject("Wiring Slime"); - slimeObject.SetActive(false); - renderer.SlimeRenderer = slimeObject.AddComponent(); - renderer.LoadZone(zone); var ore = new SimpleCommodity { Data = items.ItemData.RefOf(items.ItemData.GetByName("Wiring Ore")), Quantity = 7 }; - var floating = zone.Release(ore, new float2(3, 4), new float2(10, 0)); + var floating = zone.Release(ore, new float2(-17, 4), new float2(10, 0)); var mine = new Mine { Body = new KinematicBody { Position = new float2(-20, 8), Velocity = new float2(0, 14) }, @@ -143,6 +145,13 @@ namespace Aetheria.EditorTools }; zone.Lay(mine); + // Bound the way ZoneRenderer.LoadFloatingItem and LoadMine bind them: the prefab ARPG.unity names, Bind(zone, body, settings). + // (A ZoneRenderer would load this zone's sun and planet too, and that needs the planet prefab, the map and a GPU.) + var pickup = UnityEngine.Object.Instantiate(pickupPrefab, rootObject.transform); + pickup.Bind(zone, floating, settings); + var mineInstance = UnityEngine.Object.Instantiate(minePrefab, rootObject.transform); + mineInstance.Bind(zone, mine, settings); + // A round on its frozen line, no target and no outcome known: drawn at FireOrigin + direction * speed * (T - FireTime). var shot = new PendingShot { @@ -155,13 +164,17 @@ namespace Aetheria.EditorTools zone.Update(.5f); - var pickup = rootObject.GetComponentsInChildren(true).Single(p => p.Item == ore); - var mineInstance = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None).Single(); var pickupVelocity = floating.Body.TotalVelocity; var mineVelocity = mine.Body.TotalVelocity; Require(Mathf.Abs(pickupVelocity.x) > 1f && Mathf.Abs(mineVelocity.y) > 1f, "The fixture's bodies stopped: a lead would not move them."); + // The field under each floater really is a function of the drawn time here, by more than the tolerance at the largest lead. + const float LargestLead = .015f; + foreach (var at in new[] { floating.Body.Position + pickupVelocity * LargestLead, mine.Body.Position + mineVelocity * LargestLead }) + Require(Mathf.Abs(DrawAhead.Height(zone, at, LargestLead) - zone.GetHeight(at, zone.ExactTime)) > 5 * Tolerance, + $"The fixture's field is flat under a floater: height {DrawAhead.Height(zone, at, LargestLead)} at the drawn time, {zone.GetHeight(at, zone.ExactTime)} at the step, planet at {zone.OrbitPositionAt(planetOrbit, zone.ExactTime)}, floater at {at}."); + Vector2 baseline = default; foreach (var leadSeconds in new[] { 0f, .008f, .015f }) { @@ -176,6 +189,9 @@ namespace Aetheria.EditorTools Require(Mathf.Abs(seen.x - want.x) < Tolerance && Mathf.Abs(seen.z - want.y) < Tolerance, $"ItemPickup at lead {lead}: drawn x/z {seen.x},{seen.z}, the step plus velocity times lead {want.x},{want.y}."); + Require(Mathf.Abs(seen.y - DrawAhead.FloaterHeight(zone, settings, new float2(want.x, want.y), lead)) < Tolerance, + $"ItemPickup y {seen.y} is not on the floater layer."); + seen = mineInstance.transform.position; want = mine.Body.Position + mineVelocity * lead; Require(Mathf.Abs(seen.x - want.x) < Tolerance && Mathf.Abs(seen.z - want.y) < Tolerance, diff --git a/tests/Aetheria.Shared.Tests/PickupTests.cs b/tests/Aetheria.Shared.Tests/PickupTests.cs index 78e1d478a..af979eea5 100644 --- a/tests/Aetheria.Shared.Tests/PickupTests.cs +++ b/tests/Aetheria.Shared.Tests/PickupTests.cs @@ -64,7 +64,9 @@ public sealed class PickupTests : IDisposable }; } - private Lab Build(PickupData beam, float lifetime = 30f) + // With `wells`, the zone has a sun (a parentless rail off the ship's side, strength 400) and a planet on a moving rail round + // it (strength 25), the field a floater layer is read against; without, the zone is flat and nothing pulls on anything. + private Lab Build(PickupData beam, float lifetime = 30f, bool wells = false) { var settings = new GameplaySettings { @@ -77,7 +79,8 @@ public sealed class PickupTests : IDisposable LootDropProbability = 0, LootDropVelocity = 25, FloatingItemLifetime = lifetime, ShipLayerHeight = 3f, FloaterLayerHeight = -1.5f }; - var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true); + var id = Guid.NewGuid().ToString("N"); + var cache = AetheriaStores.Open(Path.Combine(_root, id + ".cc"), Path.Combine(_root, id + "-run.cc"), catalogWritable: true); _caches.Add(cache); cache.Upsert(new VerseGrammar { Revision = 1 }); cache.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = Solid(5, 5), Durability = 1000000, Mass = 1000 }); @@ -104,7 +107,26 @@ public sealed class PickupTests : 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, new PlanetSettings(), new ZonePack { Radius = 0 }, new GalaxyZone { Name = "Pickup", Owner = null }, null); + var pack = new ZonePack { Radius = 0 }; + var curves = new PlanetSettings(); + if (wells) + { + curves = new PlanetSettings + { + BodyRadius = new ExponentialCurve(), + WellStrength = new ExponentialCurve { Multiplier = 1f, Exponent = 1f }, + RocheLimit = new ExponentialCurve { Constant = 2f } + }; + var sunOrbit = cache.Upsert(new OrbitData { FixedPosition = float2(-50, 0) }).Key; + var planetOrbit = cache.Upsert(new OrbitData { Parent = new CultRecordRef(sunOrbit), Distance = 90f, Phase = .9f }).Key; + pack.Radius = 5000f; + pack.Mass = 10000f; + pack.Orbits.Add(new CultRecordRef(sunOrbit)); + pack.Orbits.Add(new CultRecordRef(planetOrbit)); + pack.Planets.Add(new CultRecordRef(cache.Upsert(new PlanetData { Name = "Sun", Orbit = new CultRecordRef(sunOrbit), Mass = 400f }).Key)); + pack.Planets.Add(new CultRecordRef(cache.Upsert(new PlanetData { Name = "Planet", Orbit = new CultRecordRef(planetOrbit), Mass = 25f }).Key)); + } + var zone = new Zone(items, curves, pack, new GalaxyZone { Name = "Pickup", Owner = null }, null); return new Lab { Items = items, Zone = zone }; } @@ -251,16 +273,29 @@ public sealed class PickupTests : IDisposable public void AGrabPointRidesTheFloaterLayerNotTheShips() { using var bag = new CompositeDisposable(); - var lab = Build(Beam()); + var lab = Build(Beam(), wells: true); var ship = Spawn(lab, float3(0, 0, 0)); var item = Drift(lab, 40, 0); - var events = Watch(ship, bag); + + // The fixture has a field to ride: it differs between the ship's point and the item's, and between the start and + // the end of the run, so a grab point read at the ship's xz or at the wrong time cannot agree with it. + var atShip = lab.Zone.GetHeight(float2(0, 0), lab.Zone.ExactTime); + var atItem = lab.Zone.GetHeight(float2(40, 0), lab.Zone.ExactTime); + Assert.True(abs(atItem - atShip) > 1f, $"the field is flat under the grab: ship {atShip}, item {atItem}"); + var start = lab.Zone.ExactTime; + + // Each event is checked at the zone time it was published, where the sim read the field. + var seen = new List<(GrabEvent Event, double Time)>(); + ship.Capabilities.Grab.Subscribe(e => seen.Add((e, lab.Zone.ExactTime))).AddTo(bag); ship.TractorCommand = 1; for (var tick = 1; tick <= 150; tick++) lab.Zone.Update(1 / 60f); - Assert.NotEmpty(events); - Assert.All(events, e => Assert.Equal(lab.Zone.GetHeight(e.TargetPosition.xz, lab.Zone.ExactTime) - 1.5f, e.TargetPosition.y, 3)); + Assert.NotEmpty(seen); + Assert.True(abs(lab.Zone.GetHeight(float2(40, 0), lab.Zone.ExactTime) - lab.Zone.GetHeight(float2(40, 0), start)) > .01f, "the wells did not move"); + Assert.All(seen, s => Assert.Equal(lab.Zone.GetHeight(s.Event.TargetPosition.xz, s.Time) - 1.5f, s.Event.TargetPosition.y, 3)); + Assert.All(seen, s => Assert.True(abs(s.Event.TargetPosition.y - (-1.5f)) > 1f, "the grab point sits on the bare layer, the field dropped")); } + [Fact] public void GrabEventsNameTheFloatingItem() {