diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 58d77fbb7..947a83719 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -100,6 +100,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks RunCheck(RetainedWorldAdvancesEveryFixedSubstep); RunCheck(StableEntityIdentitySurvivesCrossZoneReindex); RunCheck(TractorRampsAndPullsThroughYmirWithoutTeleportingCargo); + RunCheck(PickupProximityPrecedesYmirCollisionShell); RunCheck(PickupIsCapacityCheckedExactlyOnceAndExpires); RunCheck(StationBodiesCannotConsumePickups); RunCheck(PickupProximityCollectsOrBounces); @@ -8222,6 +8223,69 @@ internal sealed class AetheriaDaemonYmirSmokeChecks "thermal profiles must cross the CDN boundary with exact Unity paths and semantic roles"); } + private static void PickupProximityPrecedesYmirCollisionShell() + { + var hull = CatalogItem("proximity-hull"); + hull.HullCapacity = PerformanceStat(1); + var salvage = CatalogItem("proximity-salvage"); + salvage.Volume = 1; + var cargoBay = CatalogItem("proximity-cargo-bay"); + cargoBay.InteriorOccupiedCells = 1; + var catalog = new AetheriaRuntimeCatalogSnapshot([hull, salvage, cargoBay], [], []); + var ship = Entity(0, 0, "player"); + ship.HullItemKey = hull.ItemKey; + ship.CargoBays = + [ + new AetheriaRuntimeLoadoutItemSlotCommit + { + Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 } + } + ]; + ship.CargoContents = [Cargo()]; + var pickup = new AetheriaRuntimeDroppedPickupCommit + { + PickupIndex = 40, + PositionX = AetheriaRuntimeTractorMechanics.CollectionDistance, + Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = salvage.ItemKey, Quantity = 1 }, + LifetimeSeconds = 30 + }; + var run = new AetheriaRuntimeRunCheckpointCommit + { + RunId = "pickup-proximity-before-contact", + CurrentZoneIndex = 0, + CurrentEntityKey = "zone.0.entity.0", + Zones = + [ + new AetheriaRuntimeZoneSnapshotCommit + { + ZoneIndex = 0, + Entities = [ship], + DroppedPickups = [pickup] + } + ] + }; + + AetheriaRuntimeDaemonTickRunner.Tick( + Path.Combine(Path.GetTempPath(), "aetheria-pickup-before-contact.cc"), + run, + new AetheriaRuntimeDaemonTickOptions + { + Catalog = catalog, + WorldPhysics = NewPhysics(), + FrameId = 1, + FixedDeltaSeconds = 0.02, + SimulationTimeSeconds = 0.02, + BuildPublications = false + }); + + Require(AetheriaRuntimeTractorMechanics.CollectionDistance > 25, + "pickup collection must begin outside the current 20 + 5 Ymir collision shell"); + RequireEqual(1, CargoQuantity(ship, salvage.ItemKey), + "daemon XZ proximity must collect before Ymir contact can become the deciding fact"); + RequireEqual(0, run.Zones[0].DroppedPickups.Count, + "a proximity-collected pickup must leave authoritative world state exactly once"); + } + private static void PickupIsCapacityCheckedExactlyOnceAndExpires() { var hull = CatalogItem("pickup-hull"); hull.HullCapacity = PerformanceStat(1); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeTractorMechanics.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeTractorMechanics.cs index 3c59dd89a..6bcdc83d1 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeTractorMechanics.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeTractorMechanics.cs @@ -7,7 +7,10 @@ namespace GameCult.Aetheria.State.Verse public const double Radius = 25; public const double Traction = 25; public const double Distance = 75; - public const double CollectionDistance = 25; + // Collection is an XZ gameplay proximity, not a Ymir contact outcome. + // The current 20-unit ship and 5-unit pickup circles touch at 25, so + // keep the collection envelope outside that collision shell. + public const double CollectionDistance = 26; public const double RejectionKick = 25; } }