Files
kronic e23bebc84b Tests: inventory EditMode coverage (DR-026)
InventoryMathTests (stack/slot-cap/withdraw/CountOf); InventoryHarvestTests (owned harvest -> inventory with ledger untouched, full-bag spill, no-matching-player -> ledger fallback); InventoryDepositSystemTests (specific/all/unresolvable). The 8 existing ResourceHarvestSystemTests stay green via the genuine no-owner fallback. 228/228 EditMode pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:43:47 -07:00

142 lines
6.6 KiB
C#

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
{
/// <summary>
/// Plain-Entities EditMode tests for the harvest -&gt; PERSONAL inventory reroute in
/// <see cref="ResourceHarvestSystem"/>. 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 <see cref="ResourceHarvestSystemTests"/> pin the un-owned -&gt; ledger fallback.
/// </summary>
public class InventoryHarvestTests
{
static (World world, SimulationSystemGroup group, Entity ledger) MakeWorld(string name)
{
var world = new World(name);
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<ResourceHarvestSystem>());
group.SortSystems();
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
var em = world.EntityManager;
var ledger = em.CreateEntity(typeof(ResourceLedger));
em.AddBuffer<StorageEntry>(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<PlayerTag>(e);
em.AddBuffer<InventorySlot>(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<StorageEntry>(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<InventorySlot>(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<ResourceNode>(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<InventorySlot>(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<ResourceNode>(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));
}
}
}
}