Files
Project-M/Assets/_Project/Tests/EditMode/WaveSystemTests.cs
T
kronic b34945c2d2 LANTERN purge B3+B5: delete the cycle/core/win-lose spine + onboarding; save epoch v7
Deletes CyclePhaseSystem, GoalReachedSystem, CoreDamage/CoreRestore, ThreatDirector,
CoreIntegrity/GoalProgress/RunPhase/RunOutcome/ThreatState components,
CoreVisualFeedbackSystem, and the whole Client/Onboarding slice (+6 test files).

Keepers reworked: RunDirectorSystem (UpdateBefore attr + launch guard + goal/threat
bank removed; sole SaveRequest raiser now), CycleDirectorSpawnSystem (ledger/meta
host only), WaveSystem UNGATED (waves run wherever a WaveDirector is baked),
EnemyAISystem core-fallback stripped, AmbientAudioSystem reworked (bed + run cues;
no CycleState gate), MusicSystem RunInfo-only, HudSystem big trim (goal meter, core
bar, siege banner, terminal banner, outcome flash, onboarding hook all gone),
MetaShop/ClassPrep/AimReticle siege gates dropped, DebugOverlay/ops re-meant
(SpawnWave=force next wave, EndSiege=quiet arena; SetCalm/AdvanceGoal/SetHeat
retired, bytes reserved), TuningConfig Core knobs retired (ids 20-23 reserved),
StorageMath.DrainFraction deleted, HowToPlay copy rewritten.

Save epoch v7 (fresh epoch, operator-approved): SaveData drops goal/core/outcome +
conveyor/machine-IO fields; MinLoadableVersion=7; PendingSave/PendingStructure
trimmed; RollTerminalCampaignForward deleted; SaveStructureScan signature slimmed.

390 tests green; Play world-creation clean (player + waves live, no exceptions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 15:27:12 -07:00

124 lines
5.3 KiB
C#

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 director).
/// A bare world is seeded with a NetworkTime singleton 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). Waves are UNGATED
/// (LANTERN purge: the old CycleState Siege gate is deleted — placement of a WaveDirector decides).
/// Pins: a due Lull starts the next (escalating) wave; Spawning emits one Husk per interval; a
/// fully-spawned, cleared wave returns to Lull.
/// </summary>
public class WaveSystemTests
{
static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick)
{
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) });
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);
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);
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 Fully_Spawned_Cleared_Wave_Returns_To_Lull()
{
var (world, group) = MakeWorld("WaveCleared", serverTick: 100);
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.");
}
}
}
}