b34945c2d2
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>
272 lines
13 KiB
C#
272 lines
13 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 <see cref="RunDirectorSystem"/>'s Step-7 linear traversal: the objective
|
|
/// Cleared edge tears the room down AT RoomReward ENTRY and the next room spawns only on the advance (the
|
|
/// teardown-before-spawn empty-tick invariant), the ping-pong sub-slot flip + RoomEpoch bump + teleport, the boss
|
|
/// terminal, and the CLEAR-GATED once-per-RunEpoch bank (boss-clear credits Charge/RunsCompleted/retaliation +
|
|
/// save; an abort banks ONLY the honest depth high-water — D-F3/F7/C7).
|
|
/// </summary>
|
|
public class RunDirectorTraversalTests
|
|
{
|
|
const uint Seed = 777u;
|
|
const uint T0 = 2000;
|
|
|
|
static (World world, SimulationSystemGroup group, Entity dir, Entity player) MakeMidRunWorld(
|
|
int currentRoom, out RunMap map)
|
|
{
|
|
var world = new World("TraversalTest");
|
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
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) });
|
|
|
|
map = RunMapMath.Generate(Seed);
|
|
var dir = em.CreateEntity(typeof(RunInfo), typeof(RunRuntime), typeof(ExpeditionObjective),
|
|
typeof(RouteCommand), typeof(PortalCommand), typeof(MetaCounters), typeof(SaveRequest));
|
|
em.SetComponentData(dir, new RunInfo
|
|
{
|
|
Lifecycle = RunLifecycle.InRoom,
|
|
CurrentRoom = currentRoom,
|
|
RoomCount = map.LayerCount,
|
|
RunSeed = Seed,
|
|
});
|
|
em.SetComponentData(dir, new RunRuntime
|
|
{
|
|
RunSeed = Seed,
|
|
RunEpoch = 1,
|
|
RoomEpoch = currentRoom + 1,
|
|
CurrentNodeId = RunMap.NodeId(currentRoom, 0),
|
|
ActiveSubSlot = (byte)(currentRoom & 1),
|
|
RoomsClearedThisRun = currentRoom, // rooms before this one were cleared
|
|
});
|
|
|
|
// Mid-run fixture: the launch edge would have stamped the roster tag (RunParticipant) — fabricate it.
|
|
var player = em.CreateEntity(typeof(PlayerTag), typeof(PlayerReady), typeof(RegionTag),
|
|
typeof(LocalTransform), typeof(RunParticipant));
|
|
em.SetComponentData(player, new RegionTag { Region = RegionId.Expedition });
|
|
em.SetComponentData(player, LocalTransform.Identity);
|
|
return (world, group, dir, player);
|
|
}
|
|
|
|
static void SetTick(World world, uint tick)
|
|
{
|
|
var em = world.EntityManager;
|
|
var q = em.CreateEntityQuery(typeof(NetworkTime));
|
|
em.SetComponentData(q.GetSingletonEntity(), new NetworkTime { ServerTick = new NetworkTick(tick) });
|
|
q.Dispose();
|
|
}
|
|
|
|
static void MarkCleared(EntityManager em, Entity dir) =>
|
|
em.SetComponentData(dir, new ExpeditionObjective { State = ExpeditionObjectiveState.Cleared, Remaining = 0 });
|
|
|
|
// DR-046: drive the RoomExplore loot window past its portal gate (interact the portal, then tick).
|
|
static void PortalAdvance(EntityManager em, SimulationSystemGroup group, Entity dir)
|
|
{
|
|
em.SetComponentData(dir, new PortalCommand { HasInteract = 1 });
|
|
group.Update();
|
|
}
|
|
|
|
|
|
static int RoomEntities(EntityManager em)
|
|
{
|
|
var q = em.CreateEntityQuery(typeof(RoomTag));
|
|
int n = q.CalculateEntityCount();
|
|
q.Dispose();
|
|
return n;
|
|
}
|
|
|
|
[Test]
|
|
public void Cleared_LootWindowThenPortalAdvances_WithSlotFlipAndEpochBump()
|
|
{
|
|
var (world, group, dir, player) = MakeMidRunWorld(0, out var map);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
var node0 = em.CreateEntity(typeof(RoomTag));
|
|
em.SetComponentData(node0, new RoomTag { Room = 0 });
|
|
|
|
MarkCleared(em, dir);
|
|
group.Update(); // InRoom -> RoomReward (DR-046: room PERSISTS now, no teardown here)
|
|
Assert.AreEqual(RunLifecycle.RoomReward, em.GetComponentData<RunInfo>(dir).Lifecycle);
|
|
Assert.AreEqual(1, RoomEntities(em), "DR-046: the cleared room persists into the loot window");
|
|
Assert.AreEqual(1, em.GetComponentData<RunRuntime>(dir).RoomsClearedThisRun, "honest depth counter");
|
|
|
|
group.Update(); // RoomReward -> RoomExplore (loot window; portal up)
|
|
Assert.AreEqual(RunLifecycle.RoomExplore, em.GetComponentData<RunInfo>(dir).Lifecycle);
|
|
Assert.AreEqual(1, RoomEntities(em), "nodes still lootable during RoomExplore");
|
|
|
|
PortalAdvance(em, group, dir); // interact the portal -> teardown + open the route gate
|
|
var gateInfo = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreEqual(RunLifecycle.RouteSelect, gateInfo.Lifecycle, "portal advances to the branching gate");
|
|
Assert.AreEqual(0, RoomEntities(em), "room torn down AT the portal exit (the empty-tick guarantee)");
|
|
Assert.Greater((int)gateInfo.RouteOptionCount, 0, "authoritative options published");
|
|
|
|
byte pickIdx = (byte)(gateInfo.RouteOptionCount - 1);
|
|
byte expectedCol = pickIdx == 2 ? gateInfo.RouteOpt2Col
|
|
: pickIdx == 1 ? gateInfo.RouteOpt1Col : gateInfo.RouteOpt0Col;
|
|
em.SetComponentData(dir, new RouteCommand { HasPick = 1, OptionIndex = pickIdx, ForRunEpoch = 1, ForLayer = 0 });
|
|
|
|
group.Update(); // RouteSelect -> InRoom room 1 at the PICKED column
|
|
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
var run = em.GetComponentData<RunRuntime>(dir);
|
|
Assert.AreEqual(RunLifecycle.InRoom, info.Lifecycle);
|
|
Assert.AreEqual(1, info.CurrentRoom);
|
|
Assert.AreEqual(expectedCol, info.CurrentCol, "entered the PICKED column");
|
|
Assert.AreEqual(1, run.ActiveSubSlot, "ping-pong sub-slot flipped");
|
|
Assert.AreEqual(2, run.RoomEpoch, "RoomEpoch bumped so the room systems reseed");
|
|
Assert.AreEqual(RunMap.NodeId(1, expectedCol), run.CurrentNodeId, "single plan authority published");
|
|
Assert.AreEqual(map.Node(1, expectedCol).RoomType, run.CurrentRoomType);
|
|
Assert.AreEqual(0, em.GetComponentData<RouteCommand>(dir).HasPick, "latch consumed");
|
|
Assert.AreEqual(0, (int)info.RouteOptionCount, "gate closed on advance");
|
|
Assert.GreaterOrEqual(em.GetComponentData<LocalTransform>(player).Position.x, 1499f, "party teleported (+1500)");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void BossClear_Returns_AndBanksExactlyOnce_ClearGated()
|
|
{
|
|
RunMap map0;
|
|
var (world, group, dir, player) = MakeMidRunWorld(0, out map0);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
int bossLayer = map0.LayerCount - 1;
|
|
var info0 = em.GetComponentData<RunInfo>(dir);
|
|
info0.CurrentRoom = bossLayer;
|
|
em.SetComponentData(dir, info0);
|
|
var run0 = em.GetComponentData<RunRuntime>(dir);
|
|
run0.CurrentNodeId = map0.BossNodeId;
|
|
run0.ActiveSubSlot = (byte)(bossLayer & 1);
|
|
run0.RoomsClearedThisRun = bossLayer;
|
|
em.SetComponentData(dir, run0);
|
|
|
|
MarkCleared(em, dir);
|
|
group.Update(); // InRoom -> RoomReward (LastTerminalCleared = 1; room persists)
|
|
group.Update(); // RoomReward -> RoomExplore
|
|
PortalAdvance(em, group, dir); // portal -> Returning (boss cleared)
|
|
group.Update(); // Returning: bank + teleport home -> Staging
|
|
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreEqual(RunLifecycle.Staging, info.Lifecycle);
|
|
Assert.AreEqual(RegionId.Base, em.GetComponentData<RegionTag>(player).Region, "party home");
|
|
var meta = em.GetComponentData<MetaCounters>(dir);
|
|
Assert.AreEqual(1, meta.RunsCompleted, "run completed");
|
|
Assert.AreEqual(bossLayer + 1, meta.MaxDepthReached, "honest depth = rooms actually cleared");
|
|
Assert.AreEqual(1, em.GetComponentData<SaveRequest>(dir).Pending, "save checkpoint requested");
|
|
Assert.AreEqual(1, info.RunsCompleted, "HUD mirror updated");
|
|
|
|
group.Update();
|
|
group.Update();
|
|
Assert.AreEqual(1, em.GetComponentData<MetaCounters>(dir).RunsCompleted);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Abort_BanksDepthOnly_NoWinCredit()
|
|
{
|
|
var (world, group, dir, player) = MakeMidRunWorld(2, out var map);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
// All expedition players gone mid-room-2 (rooms 0-1 cleared) -> abort.
|
|
em.SetComponentData(player, new RegionTag { Region = RegionId.Base });
|
|
|
|
group.Update(); // InRoom -> Returning (abort)
|
|
group.Update(); // Returning: depth-only bank -> Staging
|
|
|
|
Assert.AreEqual(RunLifecycle.Staging, em.GetComponentData<RunInfo>(dir).Lifecycle);
|
|
var meta = em.GetComponentData<MetaCounters>(dir);
|
|
Assert.AreEqual(0, meta.RunsCompleted, "no completed-run credit");
|
|
Assert.AreEqual(2, meta.MaxDepthReached, "honest depth: the 2 rooms actually cleared, not the plan");
|
|
Assert.AreEqual(0, em.GetComponentData<SaveRequest>(dir).Pending, "no save spam on abort");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void RouteGate_PickBeatsSameTickGrace()
|
|
{
|
|
var (world, group, dir, player) = MakeMidRunWorld(0, out var map);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
MarkCleared(em, dir);
|
|
group.Update();
|
|
group.Update(); // -> RoomExplore
|
|
PortalAdvance(em, group, dir); // -> RouteSelect (route grace armed)
|
|
|
|
var gate = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreEqual(RunLifecycle.RouteSelect, gate.Lifecycle);
|
|
byte pickIdx = (byte)(gate.RouteOptionCount - 1);
|
|
byte pickedCol = pickIdx == 2 ? gate.RouteOpt2Col : pickIdx == 1 ? gate.RouteOpt1Col : gate.RouteOpt0Col;
|
|
|
|
em.SetComponentData(dir, new RouteCommand { HasPick = 1, OptionIndex = pickIdx, ForRunEpoch = 1, ForLayer = 0 });
|
|
SetTick(world, T0 + 100000);
|
|
group.Update();
|
|
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreEqual(RunLifecycle.InRoom, info.Lifecycle);
|
|
Assert.AreEqual(pickedCol, info.CurrentCol, "the accepted pick beats the same-tick grace expiry");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void RouteGate_GraceAutoPicksLowestOption()
|
|
{
|
|
var (world, group, dir, player) = MakeMidRunWorld(0, out var map);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
MarkCleared(em, dir);
|
|
group.Update();
|
|
group.Update(); // -> RoomExplore
|
|
PortalAdvance(em, group, dir); // -> RouteSelect
|
|
|
|
var gate = em.GetComponentData<RunInfo>(dir);
|
|
byte lowestCol = gate.RouteOpt0Col;
|
|
SetTick(world, T0 + 100000);
|
|
group.Update();
|
|
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreEqual(RunLifecycle.InRoom, info.Lifecycle, "the AFK backstop advances the run");
|
|
Assert.AreEqual(lowestCol, info.CurrentCol, "deterministic lowest-index reachable auto-pick");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void RouteGate_Abort_ClosesGateOnTheEdge()
|
|
{
|
|
var (world, group, dir, player) = MakeMidRunWorld(0, out var map);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
MarkCleared(em, dir);
|
|
group.Update();
|
|
group.Update(); // -> RoomExplore
|
|
PortalAdvance(em, group, dir); // -> RouteSelect
|
|
Assert.Greater((int)em.GetComponentData<RunInfo>(dir).RouteOptionCount, 0);
|
|
|
|
em.SetComponentData(player, new RegionTag { Region = RegionId.Base }); // all left
|
|
group.Update(); // RouteSelect -> Returning (abort)
|
|
|
|
var info = em.GetComponentData<RunInfo>(dir);
|
|
Assert.AreEqual(RunLifecycle.Returning, info.Lifecycle);
|
|
Assert.AreEqual(0, (int)info.RouteOptionCount, "gate closed ON the abort edge (review F3)");
|
|
}
|
|
}
|
|
}
|
|
}
|