Further Tests & Progress

This commit is contained in:
2026-06-04 11:35:57 -07:00
parent 5c11ff4fad
commit 51401d2c2b
65 changed files with 2784 additions and 45 deletions
@@ -0,0 +1,142 @@
using NUnit.Framework;
using ProjectM.Server;
using ProjectM.Simulation;
using Unity.Core;
using Unity.Entities;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Tests
{
/// <summary>
/// Plain-Entities EditMode tests for the server-only <see cref="WaveSystem"/> (Husk wave/threat director).
/// A bare world is seeded with NetworkTime + CycleState singletons and a director entity carrying
/// WaveDirector + WaveState + a WaveEnemyPrefab buffer (whose prefab is a real <c>Prefab</c>-tagged entity so
/// it is excluded from the alive-Husk query and Instantiate yields plain Husk instances). Pins: a due Lull
/// starts the next (escalating) wave; Spawning emits one Husk per interval; the director is gated off outside
/// Defend; a fully-spawned, cleared wave returns to Lull.
/// </summary>
public class WaveSystemTests
{
static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick, byte cyclePhase)
{
var world = new World(name);
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<WaveSystem>());
group.SortSystems();
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
var em = world.EntityManager;
var nt = em.CreateEntity(typeof(NetworkTime));
em.SetComponentData(nt, new NetworkTime { ServerTick = new NetworkTick(serverTick) });
var cyc = em.CreateEntity(typeof(CycleState));
em.SetComponentData(cyc, new CycleState { Phase = cyclePhase });
return (world, group);
}
static Entity MakeHuskPrefab(EntityManager em)
{
var e = em.CreateEntity(typeof(LocalTransform), typeof(EnemyTag));
em.AddComponent<Prefab>(e); // real prefab: excluded from EnemyTag queries; Instantiate strips the tag
return e;
}
static Entity MakeDirector(EntityManager em, Entity huskPrefab, byte phase, int waveNumber,
uint nextActionTick, int remainingToSpawn, int spawnCounter)
{
var e = em.CreateEntity(typeof(WaveDirector), typeof(WaveState));
em.SetComponentData(e, new WaveDirector
{
RingRadius = 10f, RingSlots = 12, BaseCount = 3, CountPerWave = 2,
SpawnIntervalTicks = 10, LullTicks = 5,
});
em.SetComponentData(e, new WaveState
{
Phase = phase, WaveNumber = waveNumber, NextActionTick = nextActionTick,
RemainingToSpawn = remainingToSpawn, SpawnCounter = spawnCounter,
});
var buf = em.AddBuffer<WaveEnemyPrefab>(e);
buf.Add(new WaveEnemyPrefab { Prefab = huskPrefab });
return e;
}
static int HuskCount(EntityManager em)
{
using var q = em.CreateEntityQuery(typeof(EnemyTag));
return q.CalculateEntityCount();
}
[Test]
public void Due_Lull_Starts_Wave_With_Escalating_Count()
{
var (world, group) = MakeWorld("WaveLullStart", serverTick: 100, cyclePhase: CyclePhase.Defend);
using (world)
{
var em = world.EntityManager;
var prefab = MakeHuskPrefab(em);
var dir = MakeDirector(em, prefab, WavePhase.Lull, waveNumber: 0, nextActionTick: 100, remainingToSpawn: 0, spawnCounter: 0);
group.Update();
var w = em.GetComponentData<WaveState>(dir);
Assert.AreEqual(WavePhase.Spawning, w.Phase, "A due lull starts spawning.");
Assert.AreEqual(1, w.WaveNumber, "Wave number advances.");
Assert.AreEqual(3, w.RemainingToSpawn, "Wave 1 count = BaseCount + (1-1)*CountPerWave = 3.");
Assert.AreEqual(0, HuskCount(em), "No Husk is spawned on the lull->spawning transition tick itself.");
}
}
[Test]
public void Spawning_Emits_One_Husk_And_Decrements_Remaining()
{
var (world, group) = MakeWorld("WaveSpawnOne", serverTick: 100, cyclePhase: CyclePhase.Defend);
using (world)
{
var em = world.EntityManager;
var prefab = MakeHuskPrefab(em);
var dir = MakeDirector(em, prefab, WavePhase.Spawning, waveNumber: 1, nextActionTick: 100, remainingToSpawn: 3, spawnCounter: 0);
group.Update();
Assert.AreEqual(1, HuskCount(em), "One Husk spawns this interval.");
var w = em.GetComponentData<WaveState>(dir);
Assert.AreEqual(2, w.RemainingToSpawn, "RemainingToSpawn decrements.");
Assert.AreEqual(1, w.SpawnCounter, "SpawnCounter advances (drives ring slot + round-robin).");
}
}
[Test]
public void Director_Is_Gated_Off_Outside_Defend()
{
var (world, group) = MakeWorld("WaveGated", serverTick: 100, cyclePhase: CyclePhase.Expedition);
using (world)
{
var em = world.EntityManager;
var prefab = MakeHuskPrefab(em);
var dir = MakeDirector(em, prefab, WavePhase.Lull, waveNumber: 0, nextActionTick: 100, remainingToSpawn: 0, spawnCounter: 0);
group.Update();
var w = em.GetComponentData<WaveState>(dir);
Assert.AreEqual(WavePhase.Lull, w.Phase, "Outside Defend the director does not run.");
Assert.AreEqual(0, w.WaveNumber, "Wave number stays put outside Defend.");
}
}
[Test]
public void Fully_Spawned_Cleared_Wave_Returns_To_Lull()
{
var (world, group) = MakeWorld("WaveCleared", serverTick: 100, cyclePhase: CyclePhase.Defend);
using (world)
{
var em = world.EntityManager;
var prefab = MakeHuskPrefab(em);
var dir = MakeDirector(em, prefab, WavePhase.Spawning, waveNumber: 1, nextActionTick: 100, remainingToSpawn: 0, spawnCounter: 3);
group.Update();
Assert.AreEqual(WavePhase.Lull, em.GetComponentData<WaveState>(dir).Phase,
"A fully-spawned wave with no live Husks returns to Lull.");
}
}
}
}