Files
Project-M/Assets/_Project/Tests/EditMode/ReadyCheckSystemTests.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

206 lines
8.6 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 ready-check spine: <see cref="ReadyToggleSystem"/> (RPC → PlayerReady,
/// Staging/Launching-only) ordered before <see cref="RunDirectorSystem"/> (the all-ready rising-edge launch, the
/// un-ready countdown abort, the F2 outcome guard, the sub-slot teleport out/home, and the Returning-edge
/// ready-flag clear). Ticks are driven manually through NetworkTime, so the countdown/dwell paths that Play-mode
/// polling races past are pinned deterministically here.
/// </summary>
public class ReadyCheckSystemTests
{
const uint T0 = 1000;
static (World world, SimulationSystemGroup group, Entity dir) MakeWorld()
{
var world = new World("ReadyCheckTest");
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<ReadyToggleSystem>());
group.AddSystemToUpdateList(world.GetOrCreateSystem<RunDirectorSystem>());
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(T0) });
var dir = em.CreateEntity(typeof(RunInfo), typeof(RunRuntime));
em.SetComponentData(dir, new RunInfo { Lifecycle = RunLifecycle.Staging });
em.SetComponentData(dir, new RunRuntime { HostSalt = 1u });
return (world, group, dir);
}
static void SetTick(World world, uint tick)
{
var em = world.EntityManager;
using var q = em.CreateEntityQuery(typeof(NetworkTime));
em.SetComponentData(q.GetSingletonEntity(), new NetworkTime { ServerTick = new NetworkTick(tick) });
}
static Entity MakePlayer(EntityManager em, int networkId)
{
var e = em.CreateEntity(typeof(PlayerTag), typeof(PlayerReady), typeof(GhostOwner),
typeof(RegionTag), typeof(LocalTransform));
em.SetComponentData(e, new GhostOwner { NetworkId = networkId });
em.SetComponentData(e, new RegionTag { Region = RegionId.Base });
em.SetComponentData(e, LocalTransform.Identity);
return e;
}
static Entity MakeConnection(EntityManager em, int networkId)
{
var e = em.CreateEntity(typeof(NetworkId));
em.SetComponentData(e, new NetworkId { Value = networkId });
return e;
}
static void SendToggle(EntityManager em, Entity conn, byte ready)
{
var e = em.CreateEntity(typeof(ReadyToggleRequest), typeof(ReceiveRpcCommandRequest));
em.SetComponentData(e, new ReadyToggleRequest { Ready = ready });
em.SetComponentData(e, new ReceiveRpcCommandRequest { SourceConnection = conn });
}
static int PendingRequests(EntityManager em)
{
using var q = em.CreateEntityQuery(typeof(ReadyToggleRequest));
return q.CalculateEntityCount();
}
[Test]
public void Toggle_SetsReady_AndSoloLaunches_SameTick()
{
var (world, group, dir) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var player = MakePlayer(em, 1);
var conn = MakeConnection(em, 1);
SendToggle(em, conn, 1);
group.Update();
Assert.AreEqual(1, em.GetComponentData<PlayerReady>(player).Value, "toggle landed");
Assert.AreEqual(0, PendingRequests(em), "request consumed");
var info = em.GetComponentData<RunInfo>(dir);
Assert.AreEqual(RunLifecycle.Launching, info.Lifecycle, "1/1 ready -> rising edge -> Launching");
Assert.AreNotEqual(0u, info.LaunchTick, "countdown telegraph armed");
Assert.AreNotEqual(0u, info.RunSeed, "run seeded");
Assert.GreaterOrEqual(info.RoomCount, 6, "seed-varied length floor");
Assert.LessOrEqual(info.RoomCount, 10, "seed-varied length cap");
}
}
[Test]
public void Toggle_Ignored_MidRun()
{
var (world, group, dir) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var player = MakePlayer(em, 1);
var conn = MakeConnection(em, 1);
var info = em.GetComponentData<RunInfo>(dir);
info.Lifecycle = RunLifecycle.InRoom;
em.SetComponentData(dir, info);
SendToggle(em, conn, 1);
group.Update();
Assert.AreEqual(0, em.GetComponentData<PlayerReady>(player).Value, "mid-run toggle dropped");
Assert.AreEqual(0, PendingRequests(em), "request still consumed");
}
}
[Test]
public void PartialReady_DoesNotLaunch()
{
var (world, group, dir) = MakeWorld();
using (world)
{
var em = world.EntityManager;
MakePlayer(em, 1);
MakePlayer(em, 2);
var conn1 = MakeConnection(em, 1);
SendToggle(em, conn1, 1);
group.Update();
Assert.AreEqual(RunLifecycle.Staging, em.GetComponentData<RunInfo>(dir).Lifecycle,
"1/2 ready must NOT launch");
}
}
[Test]
public void UnReady_DuringCountdown_Aborts()
{
var (world, group, dir) = MakeWorld();
using (world)
{
var em = world.EntityManager;
MakePlayer(em, 1);
var conn = MakeConnection(em, 1);
SendToggle(em, conn, 1);
group.Update();
Assert.AreEqual(RunLifecycle.Launching, em.GetComponentData<RunInfo>(dir).Lifecycle);
SendToggle(em, conn, 0); // change of heart during the 3-2-1
group.Update();
var info = em.GetComponentData<RunInfo>(dir);
Assert.AreEqual(RunLifecycle.Staging, info.Lifecycle, "un-ready aborts the countdown");
Assert.AreEqual(0u, info.LaunchTick, "telegraph cleared");
}
}
[Test]
public void Launch_TeleportsPartyOut_ThenHome_AndClearsReady()
{
var (world, group, dir) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var player = MakePlayer(em, 1);
var conn = MakeConnection(em, 1);
SendToggle(em, conn, 1);
group.Update(); // Staging -> Launching (countdown armed at T0)
SetTick(world, T0 + 200); // past the 180-tick countdown
group.Update(); // enter room 0 (the real traversal, Step 7)
Assert.AreEqual(RegionId.Expedition, em.GetComponentData<RegionTag>(player).Region,
"party region flipped to Expedition");
Assert.GreaterOrEqual(em.GetComponentData<LocalTransform>(player).Position.x, 999f,
"party teleported to the expedition room origin (sub-slot 0 at +1000)");
Assert.AreEqual(1f, em.GetComponentData<LocalTransform>(player).Scale, 1e-4f,
"Scale preserved through the teleport (never FromPosition)");
Assert.AreEqual(RunLifecycle.InRoom, em.GetComponentData<RunInfo>(dir).Lifecycle, "room 0 active");
// Simulate the all-left abort (disconnect edge): drop the player's region externally.
em.SetComponentData(player, new RegionTag { Region = RegionId.Base });
SetTick(world, T0 + 260);
group.Update(); // InRoom -> Returning (abort, no credit)
SetTick(world, T0 + 320);
group.Update(); // Returning: teleport home + clear ready flags -> StagingStaging
Assert.AreEqual(RegionId.Base, em.GetComponentData<RegionTag>(player).Region, "back home");
Assert.Less(em.GetComponentData<LocalTransform>(player).Position.x, 100f, "position restored to base");
Assert.AreEqual(0, em.GetComponentData<PlayerReady>(player).Value, "ready flag cleared on return");
var run = em.GetComponentData<RunRuntime>(dir);
Assert.AreEqual(run.RunEpoch, run.LastBankedRunEpoch, "terminal bank latch fired once");
Assert.AreEqual(RunLifecycle.Staging, em.GetComponentData<RunInfo>(dir).Lifecycle);
}
}
}
}