using NUnit.Framework; using ProjectM.Server; using ProjectM.Simulation; using Unity.Core; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Tests { /// /// Plain-Entities EditMode tests for the harvest -> PERSONAL inventory reroute in /// . A bare world is seeded with a ResourceLedger singleton, a node, /// a player (GhostOwner + InventorySlot + PlayerTag) and an OWNED projectile (matching GhostOwner). Pins: /// an owned hit lands in the player's inventory and leaves the ledger untouched; a full bag spills the /// remainder to the ledger; an owned projectile whose NetworkId has no live player falls back to the ledger. /// The 8 owner-less tests in pin the un-owned -> ledger fallback. /// public class InventoryHarvestTests { static (World world, SimulationSystemGroup group, Entity ledger) MakeWorld(string name) { var world = new World(name); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); group.SortSystems(); world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); var em = world.EntityManager; var ledger = em.CreateEntity(typeof(ResourceLedger)); em.AddBuffer(ledger); return (world, group, ledger); } static Entity MakeNode(EntityManager em, float3 pos, float hitRadius, byte resourceId, int remaining, float perHit) { var e = em.CreateEntity(); em.AddComponentData(e, LocalTransform.FromPosition(pos)); em.AddComponentData(e, new HitRadius { Value = hitRadius }); em.AddComponentData(e, new ResourceNode { ResourceId = resourceId, Remaining = remaining, HarvestPerHit = perHit }); return e; } static Entity MakePlayer(EntityManager em, int networkId) { var e = em.CreateEntity(); em.AddComponentData(e, new GhostOwner { NetworkId = networkId }); em.AddComponent(e); em.AddBuffer(e); return e; } static Entity MakeOwnedProjectile(EntityManager em, float3 pos, float2 dir, float lastStep, int networkId) { var e = em.CreateEntity(); em.AddComponentData(e, LocalTransform.FromPosition(pos)); em.AddComponentData(e, new Projectile { Direction = dir, LastStep = lastStep }); em.AddComponentData(e, new GhostOwner { NetworkId = networkId }); return e; } static int LedgerCount(EntityManager em, Entity ledger, ushort itemId) { var buf = em.GetBuffer(ledger); for (int i = 0; i < buf.Length; i++) if (buf[i].ItemId == itemId) return buf[i].Count; return 0; } static int InvCount(EntityManager em, Entity player, ushort itemId) { var buf = em.GetBuffer(player); return InventoryMath.CountOf(buf, itemId); } [Test] public void Owned_Harvest_Lands_In_Player_Inventory_Ledger_Untouched() { var (world, group, ledger) = MakeWorld("OwnedHarvest"); using (world) { var em = world.EntityManager; var player = MakePlayer(em, networkId: 1); var node = MakeNode(em, new float3(10, 1, 10), 1f, ResourceId.Aether, remaining: 100, perHit: 25f); var proj = MakeOwnedProjectile(em, new float3(10, 1, 10), new float2(1, 0), 5f, networkId: 1); group.Update(); Assert.AreEqual(25, InvCount(em, player, ResourceId.Aether), "The owner's harvest lands in their personal inventory."); Assert.AreEqual(0, LedgerCount(em, ledger, ResourceId.Aether), "The shared ledger is untouched by an owned harvest."); Assert.AreEqual(75, em.GetComponentData(node).Remaining); Assert.IsFalse(em.Exists(proj), "The projectile is consumed."); } } [Test] public void Full_Bag_Spills_Remainder_To_Ledger() { var (world, group, ledger) = MakeWorld("FullBagSpill"); using (world) { var em = world.EntityManager; var player = MakePlayer(em, networkId: 1); // Fill all InventoryMaxSlots with distinct dummy items so no slot is free for the harvested id. var bag = em.GetBuffer(player); for (int i = 0; i < Tuning.InventoryMaxSlots; i++) bag.Add(new InventorySlot { ItemId = (ushort)(100 + i), Count = 1 }); var node = MakeNode(em, new float3(10, 1, 10), 1f, ResourceId.Ore, remaining: 100, perHit: 25f); var proj = MakeOwnedProjectile(em, new float3(10, 1, 10), new float2(1, 0), 5f, networkId: 1); group.Update(); Assert.AreEqual(0, InvCount(em, player, ResourceId.Ore), "A full bag cannot take the harvested item."); Assert.AreEqual(25, LedgerCount(em, ledger, ResourceId.Ore), "The full amount spills to the shared ledger."); Assert.AreEqual(75, em.GetComponentData(node).Remaining, "The node decrements by the FULL amount, not just the part that fit."); Assert.IsFalse(em.Exists(proj)); } } [Test] public void Owned_Projectile_With_No_Matching_Player_Falls_Back_To_Ledger() { var (world, group, ledger) = MakeWorld("NoMatchingPlayer"); using (world) { var em = world.EntityManager; var player = MakePlayer(em, networkId: 1); var node = MakeNode(em, new float3(10, 1, 10), 1f, ResourceId.Aether, remaining: 100, perHit: 25f); // Projectile owned by NetworkId 99 — no live player has that id. var proj = MakeOwnedProjectile(em, new float3(10, 1, 10), new float2(1, 0), 5f, networkId: 99); group.Update(); Assert.AreEqual(0, InvCount(em, player, ResourceId.Aether), "Player 1 gets nothing — it didn't fire this shot."); Assert.AreEqual(25, LedgerCount(em, ledger, ResourceId.Aether), "An unresolvable owner falls back to the shared ledger."); Assert.IsFalse(em.Exists(proj)); } } } }