154 lines
7.0 KiB
C#
154 lines
7.0 KiB
C#
using NUnit.Framework;
|
|
using ProjectM.Server;
|
|
using ProjectM.Simulation;
|
|
using Unity.Collections;
|
|
using Unity.Core;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.Transforms;
|
|
|
|
namespace ProjectM.Tests
|
|
{
|
|
/// <summary>
|
|
/// Plain-Entities EditMode tests for <see cref="RoomFieldSystem"/> (the Step-5 successor of the retired
|
|
/// ExpeditionFieldSystem + its teardown regression). Pins: exactly one scatter per <c>RoomEpoch</c> (int-equality
|
|
/// reseed), the run-wide scarcity budget flooring + spend-down, RoomTag stamping + baked-Scale preservation +
|
|
/// in-shape placement at the active sub-slot origin, and the Staging defensive sweep that kills room ghosts while
|
|
/// UNTAGGED entities (the old base-field-survives regression, now structural) are untouched.
|
|
/// </summary>
|
|
public class RoomFieldSystemTests
|
|
{
|
|
const uint Seed = 777u;
|
|
|
|
static (World world, SimulationSystemGroup group, Entity dir, Entity spawnerE, Entity prefab) MakeWorld(
|
|
int nodeBudget)
|
|
{
|
|
var world = new World("RoomFieldTest");
|
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<RoomFieldSystem>());
|
|
group.SortSystems();
|
|
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
|
|
var em = world.EntityManager;
|
|
|
|
var map = RunMapMath.Generate(Seed);
|
|
var dir = em.CreateEntity(typeof(RunInfo), typeof(RunRuntime));
|
|
em.SetComponentData(dir, new RunInfo
|
|
{
|
|
Lifecycle = RunLifecycle.InRoom,
|
|
CurrentRoom = 0,
|
|
RoomCount = map.LayerCount,
|
|
});
|
|
em.SetComponentData(dir, new RunRuntime
|
|
{
|
|
RunSeed = Seed,
|
|
RoomEpoch = 1,
|
|
CurrentNodeId = RunMap.NodeId(0, 0),
|
|
ActiveSubSlot = 0,
|
|
NodeBudgetRemaining = nodeBudget,
|
|
});
|
|
|
|
// Node ghost prefab: Scale=2 pins the WithPosition (never FromPosition) preservation.
|
|
var prefab = em.CreateEntity(typeof(LocalTransform), typeof(ResourceNode));
|
|
em.SetComponentData(prefab, new LocalTransform { Position = float3.zero, Rotation = quaternion.identity, Scale = 2f });
|
|
em.SetComponentData(prefab, new ResourceNode { ResourceId = ResourceId.Ore, Remaining = 30, HarvestPerHit = 5f });
|
|
em.AddComponent<Prefab>(prefab);
|
|
|
|
// Spawner singleton with the runtime state PRE-attached (skips the one-shot attach tick).
|
|
var spawnerE = em.CreateEntity(typeof(ResourceFieldSpawner), typeof(RoomFieldState));
|
|
em.SetComponentData(spawnerE, new ResourceFieldSpawner { Prefab = prefab, Count = 99, Radius = 0f });
|
|
|
|
return (world, group, dir, spawnerE, prefab);
|
|
}
|
|
|
|
static int LiveNodes(EntityManager em)
|
|
{
|
|
using var q = em.CreateEntityQuery(typeof(ResourceNode), typeof(RoomTag));
|
|
return q.CalculateEntityCount();
|
|
}
|
|
|
|
[Test]
|
|
public void Spawns_OncePerRoomEpoch_PlanCount_TaggedInShape_ScalePreserved()
|
|
{
|
|
var (world, group, dir, spawnerE, prefab) = MakeWorld(nodeBudget: 12);
|
|
var em = world.EntityManager;
|
|
var map = RunMapMath.Generate(Seed);
|
|
var plan = RoomLayoutMath.Plan(map.NodeAt(RunMap.NodeId(0, 0)), 0, map.LayerCount);
|
|
int expected = math.min(plan.NodeCount, 12);
|
|
float3 origin = RegionMath.ExpeditionRoomOrigin(new float3(0f, 1f, 0f), 0);
|
|
|
|
group.Update();
|
|
Assert.AreEqual(expected, LiveNodes(em), "one room's plan-count scatter");
|
|
|
|
using (var q = em.CreateEntityQuery(typeof(ResourceNode), typeof(RoomTag), typeof(LocalTransform)))
|
|
{
|
|
var tags = q.ToComponentDataArray<RoomTag>(Allocator.Temp);
|
|
var xfs = q.ToComponentDataArray<LocalTransform>(Allocator.Temp);
|
|
for (int i = 0; i < tags.Length; i++)
|
|
{
|
|
Assert.AreEqual(0, tags[i].Room, "stamped with the active room index");
|
|
Assert.AreEqual(2f, xfs[i].Scale, 1e-4f, "baked Scale preserved (WithPosition, never FromPosition)");
|
|
Assert.IsTrue(RoomLayoutMath.ContainsPoint(plan.ShapeId, origin, xfs[i].Position),
|
|
"scattered inside the room shape at the sub-slot-0 origin");
|
|
Assert.GreaterOrEqual(xfs[i].Position.x, 900f, "placed at the EXPEDITION origin, not the base");
|
|
}
|
|
tags.Dispose();
|
|
xfs.Dispose();
|
|
}
|
|
|
|
Assert.AreEqual(12 - expected, em.GetComponentData<RunRuntime>(dir).NodeBudgetRemaining,
|
|
"budget spent down by exactly the scattered count");
|
|
|
|
group.Update(); // same RoomEpoch — must NOT scatter again
|
|
Assert.AreEqual(expected, LiveNodes(em), "int-equality reseed: one scatter per RoomEpoch");
|
|
world.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void Budget_FloorsSpawnCount_AndExhausts()
|
|
{
|
|
var (world, group, dir, spawnerE, prefab) = MakeWorld(nodeBudget: 1);
|
|
var em = world.EntityManager;
|
|
|
|
group.Update();
|
|
Assert.AreEqual(1, LiveNodes(em), "budget of 1 floors the room to a single node");
|
|
Assert.AreEqual(0, em.GetComponentData<RunRuntime>(dir).NodeBudgetRemaining);
|
|
|
|
// Advance to the next room with a DRY budget — nothing more may spawn.
|
|
var run = em.GetComponentData<RunRuntime>(dir);
|
|
run.RoomEpoch = 2;
|
|
run.CurrentNodeId = RunMap.NodeId(1, 0);
|
|
run.ActiveSubSlot = 1;
|
|
em.SetComponentData(dir, run);
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
info.CurrentRoom = 1;
|
|
em.SetComponentData(dir, info);
|
|
|
|
group.Update();
|
|
Assert.AreEqual(1, LiveNodes(em), "a dry budget spawns nothing (scarcity holds run-wide)");
|
|
world.Dispose();
|
|
}
|
|
|
|
[Test]
|
|
public void StagingSweep_KillsRoomGhosts_SparesUntagged()
|
|
{
|
|
var (world, group, dir, spawnerE, prefab) = MakeWorld(nodeBudget: 12);
|
|
var em = world.EntityManager;
|
|
group.Update();
|
|
Assert.Greater(LiveNodes(em), 0, "room content exists");
|
|
|
|
// An untagged node (e.g. the base mining field) must be structurally untouchable.
|
|
var baseNode = em.CreateEntity(typeof(LocalTransform), typeof(ResourceNode));
|
|
em.SetComponentData(baseNode, LocalTransform.FromPosition(new float3(20f, 0f, 0f)));
|
|
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
info.Lifecycle = RunLifecycle.Staging;
|
|
em.SetComponentData(dir, info);
|
|
|
|
group.Update();
|
|
Assert.AreEqual(0, LiveNodes(em), "Staging sweep cleared every room ghost");
|
|
Assert.IsTrue(em.Exists(baseNode), "untagged entities survive (the old base-field regression, structural now)");
|
|
world.Dispose();
|
|
}
|
|
}
|
|
}
|