diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index e02e5c50d..d838b6eaf 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -1,5 +1,6 @@ using Aetheria.State.Daemon; using GameCult.Aetheria.State.Verse; +using System.Globalization; var checks = new AetheriaDaemonYmirSmokeChecks(); checks.Run(); @@ -814,6 +815,15 @@ internal sealed class AetheriaDaemonYmirSmokeChecks Require(pickup.VelocityX < 0 && pickup.PositionX < 60, "Ymir must pull a pickup inside the forward tractor volume toward the ship"); RequireEqual(1, pickup.Item.Quantity, "tractor force must not consume the pickup item"); RequireEqual(0, CargoQuantity(ship, "salvage"), "scooping must remain a separate capacity-checked transaction"); + var frame = new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run }; + var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(frame, new AetheriaRuntimeDaemonHealthDocument(), AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon")); + var pickupNode = Flatten(surface.Surface.Root).Single(node => node.Id == "aetheria.daemon.game.world.pickup.3"); + RequireEqual("prefab.entity.pickup", pickupNode.Props["assetRef"], "Eve world must reference provider-owned pickup asset semantics"); + RequireEqual("salvage", pickupNode.Props["itemKey"], "Eve pickup must expose item identity"); + Require(double.Parse(pickupNode.Props["remainingLifetime"], CultureInfo.InvariantCulture) < 30, + "Eve pickup must expose daemon-owned remaining lifetime"); + Require(AetheriaRuntimeAssets.ProjectManifest(null).Assets.Any(asset => asset.Ref.AssetKey == "prefab.entity.pickup"), + "provider asset manifest must advertise the pickup visual used by Eve"); } private static void PickupIsCapacityCheckedExactlyOnceAndExpires() diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs index 00a0d0433..55e5aa6e7 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs @@ -27,6 +27,7 @@ namespace GameCult.Aetheria.State.Verse Add(entries, MapPrefab("prefab.entity.station", "Station", "Prefabs/Stations/AsteroidOutpost")); Add(entries, MapPrefab("prefab.entity.orbital", "Orbital", "Prefabs/Stations/Zenith")); Add(entries, MapPrefab("prefab.entity.projectile", "Projectile", "Prefabs/Lightning")); + Add(entries, MapPrefab("prefab.entity.pickup", "Pickup", "Assets/Prefabs/RPG/Pickups/Tetrahedron")); foreach (var inventoryAsset in InventoryUiAssets()) Add(entries, inventoryAsset); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs index f5ead8a29..b3ac408b7 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs @@ -461,6 +461,9 @@ namespace GameCult.Aetheria.State.Verse .Concat((zone.Projectiles ?? Array.Empty()) .Where(projectile => projectile != null && projectile.LifetimeSeconds > 0) .Select(projectile => PlayableWorldProjectile(projectile, run, zone))) + .Concat((zone.DroppedPickups ?? Array.Empty()) + .Where(pickup => pickup != null && pickup.AgeSeconds < pickup.LifetimeSeconds) + .Select(pickup => PlayableWorldPickup(pickup, run, zone))) .ToArray(); return new AetheriaRuntimeSurfaceComponent( @@ -589,6 +592,36 @@ namespace GameCult.Aetheria.State.Verse Array.Empty()); } + private static AetheriaRuntimeSurfaceComponent PlayableWorldPickup( + AetheriaRuntimeDroppedPickupCommit pickup, + AetheriaRuntimeRunCheckpointCommit run, + AetheriaRuntimeZoneSnapshotCommit zone) + { + var pickupId = $"{run.RunId}:zone:{zone.ZoneIndex}:pickup:{pickup.PickupIndex}"; + var props = new Dictionary(StringComparer.Ordinal) + { + ["entityId"] = pickupId, + ["entityKind"] = "pickup", + ["label"] = pickup.Item?.ItemKey ?? "Pickup", + ["itemKey"] = pickup.Item?.ItemKey ?? "", + ["quantity"] = Math.Max(1, pickup.Item?.Quantity ?? 1).ToString(CultureInfo.InvariantCulture), + ["assetRef"] = "prefab.entity.pickup", + ["position"] = string.Join(",", new[] { pickup.PositionX.ToString("0.###", CultureInfo.InvariantCulture), pickup.PositionY.ToString("0.###", CultureInfo.InvariantCulture), pickup.PositionZ.ToString("0.###", CultureInfo.InvariantCulture) }), + ["velocity"] = string.Join(",", new[] { pickup.VelocityX.ToString("0.###", CultureInfo.InvariantCulture), pickup.VelocityY.ToString("0.###", CultureInfo.InvariantCulture), pickup.VelocityZ.ToString("0.###", CultureInfo.InvariantCulture) }), + ["radius"] = "5", + ["age"] = FormatNumber(pickup.AgeSeconds), + ["lifetime"] = FormatNumber(pickup.LifetimeSeconds), + ["remainingLifetime"] = FormatNumber(Math.Max(0, pickup.LifetimeSeconds - pickup.AgeSeconds)), + ["selectable"] = "false", + ["controllable"] = "false" + }; + return new AetheriaRuntimeSurfaceComponent( + $"aetheria.daemon.game.world.pickup.{pickup.PickupIndex}", + "world.entity3d", + props, + Array.Empty()); + } + private static string PlayableWorldAssetRef(AetheriaRuntimeEntitySnapshotCommit entity) { if (entity == null)