Hygiene B6b: convert leaky-world tests + HudSystem cosmetics

- 10 EditMode files: every bare trailing world.Dispose() now inside using(world){} (single-world) or a [TearDown]+List<World> (the multi-world reject-matrix tests in BoonApplyTests/RouteSelectSystemTests) — an assertion failure can no longer leak the World and mask the true first failure. No test logic changed.
- HudSystem: collapse the blank-line run before the class close; trim the extracted route-map clause from the READY-panel comment.

466/466 EditMode tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 21:06:33 -07:00
parent 6379f5d897
commit e27a495530
11 changed files with 128 additions and 62 deletions
@@ -72,9 +72,7 @@ namespace ProjectM.Client
// END-2: terminal win/loss banner (observes the replicated RunOutcome; latched server-side).
VisualElement _runBanner;
Label _runBannerText, _runBannerSub;
// Demo polish: the clickable READY panel (Staging/Launching) + the drawn branching route map
// (RouteSelect) — the map is regenerated client-side from RunInfo.RunSeed for DISPLAY only; the
// clickable next-layer nodes bind to the authoritative RouteOpt* bytes (never the regen).
// Demo polish: the clickable READY panel (Staging/Launching).
VisualElement _readyPanel, _readyPipRow;
Button _readyBtn;
Label _readyTitle;
@@ -1566,23 +1564,5 @@ namespace ProjectM.Client
bool _wentInRun;
float _runFailedUntil;
}
}
@@ -6,6 +6,7 @@ using Unity.Core;
using Unity.Entities;
using Unity.NetCode;
using Unity.Transforms;
using System.Collections.Generic;
namespace ProjectM.Tests
{
@@ -18,6 +19,14 @@ namespace ProjectM.Tests
public class BoonApplyTests
{
const uint T0 = 3000;
readonly List<World> _worlds = new();
[TearDown]
public void Cleanup()
{
foreach (var w in _worlds) if (w.IsCreated) w.Dispose();
_worlds.Clear();
}
static (World world, SimulationSystemGroup group, Entity dir, Entity catalog) MakeWorld(byte lifecycle)
{
@@ -72,6 +81,8 @@ namespace ProjectM.Tests
public void ValidPick_AppendsBoonBandRow_AndClearsPending()
{
var (world, group, dir, catalog) = MakeWorld(RunLifecycle.RoomReward);
using (world)
{
var em = world.EntityManager;
var player = MakePicker(em, 1);
SendPick(em, 1, index: 1); // Option1 = id 4 (Fleet Foot, MoveSpeed +12%)
@@ -85,7 +96,7 @@ namespace ProjectM.Tests
Assert.AreEqual(0.12f, mods[0].Value, 1e-4f);
Assert.AreEqual(0, em.GetComponentData<BoonOffer>(player).Pending, "pick consumed");
Assert.AreEqual(1u, em.GetComponentData<RunRuntime>(dir).BoonPickCounter, "band provenance advanced");
world.Dispose();
}
}
[Test]
@@ -93,28 +104,30 @@ namespace ProjectM.Tests
{
// Not pending.
var (w1, g1, d1, c1) = MakeWorld(RunLifecycle.RoomReward);
_worlds.Add(w1);
var p1 = MakePicker(w1.EntityManager, 1);
w1.EntityManager.SetComponentData(p1, new BoonOffer { Pending = 0, Option0 = 1 });
SendPick(w1.EntityManager, 1, 0);
g1.Update();
Assert.AreEqual(0, BoonRows(w1.EntityManager, p1), "not-pending pick rejected");
w1.Dispose();
// Lifecycle closed (Returning): the straggler pick dies BEFORE any strip could be out-run (D-F4).
var (w2, g2, d2, c2) = MakeWorld(RunLifecycle.Returning);
_worlds.Add(w2);
var p2 = MakePicker(w2.EntityManager, 1);
SendPick(w2.EntityManager, 1, 0);
g2.Update();
Assert.AreEqual(0, BoonRows(w2.EntityManager, p2), "closed-lifecycle pick rejected");
using (var q = w2.EntityManager.CreateEntityQuery(typeof(BoonPickRequest)))
Assert.AreEqual(0, q.CalculateEntityCount(), "request still consumed");
w2.Dispose();
}
[Test]
public void GraceElapsed_AutoPicksOption0_ForPendingExpeditionPlayers()
{
var (world, group, dir, catalog) = MakeWorld(RunLifecycle.RoomReward);
using (world)
{
var em = world.EntityManager;
var afk = MakePicker(em, 1, o0: 5); // Option0 = id 5 (Iron Constitution, +25 MaxHealth)
var run = em.GetComponentData<RunRuntime>(dir);
@@ -127,7 +140,7 @@ namespace ProjectM.Tests
var mods = em.GetBuffer<StatModifier>(afk);
Assert.AreEqual((byte)StatTarget.MaxHealth, mods[0].Target);
Assert.AreEqual(0, em.GetComponentData<BoonOffer>(afk).Pending, "gate released");
world.Dispose();
}
}
[Test]
@@ -135,6 +148,8 @@ namespace ProjectM.Tests
{
// Drive the REAL RunDirectorSystem Returning edge over a player carrying all four bands.
var world = new World("BoonStripTest");
using (world)
{
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<RunDirectorSystem>());
group.SortSystems();
@@ -168,7 +183,7 @@ namespace ProjectM.Tests
&& after[i].SourceId < Tuning.BoonSourceIdBase + Tuning.BoonSourceIdSpan, "no boon-band survivor");
Assert.AreEqual(0, em.GetComponentData<BoonOffer>(player).Pending, "straggler offer zeroed");
Assert.AreEqual(RunLifecycle.Staging, em.GetComponentData<RunInfo>(dir).Lifecycle);
world.Dispose();
}
}
}
}
@@ -55,6 +55,8 @@ namespace ProjectM.Tests
public void OfferSystem_DealsOncePerRoom_ExpeditionOnly_PerPlayerSeeds()
{
var world = new World("BoonOfferTest");
using (world)
{
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<BoonOfferSystem>());
group.SortSystems();
@@ -98,7 +100,7 @@ namespace ProjectM.Tests
group.Update();
Assert.AreEqual(0, em.GetComponentData<BoonOffer>(out1).Pending, "one deal per RoomEpoch (latch)");
world.Dispose();
}
}
}
}
@@ -67,6 +67,8 @@ namespace ProjectM.Tests
public void Spawn_SeedsClassTiers_SkipsOthers_ClampsOverMax()
{
var (world, group) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var prefab = MakePlayerPrefab(em);
var spawnerE = em.CreateEntity(typeof(PlayerSpawner));
@@ -104,13 +106,15 @@ namespace ProjectM.Tests
Assert.AreEqual(2, metaRows, "exactly the two legal Warrior tiers seeded (unknown/other-class/other-mask skipped)");
Assert.AreEqual(30f, frameValue, 1e-3f, "Reinforced Frame tier 2 = 15 * 2");
Assert.AreEqual(0.40f, mightValue, 1e-3f, "Warrior's Might CLAMPED to MaxTier 4 = 0.10 * 4 (D-F5)");
world.Dispose();
}
}
[Test]
public void MissingCatalog_BlocksSpawn_PreservesRequest()
{
var (world, group) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var prefab = MakePlayerPrefab(em);
var spawnerE = em.CreateEntity(typeof(PlayerSpawner));
@@ -125,7 +129,7 @@ namespace ProjectM.Tests
var rq = em.CreateEntityQuery(typeof(GoInGameRequest));
Assert.AreEqual(1, rq.CalculateEntityCount(), "request PRESERVED — the spawn retries when the catalog streams in");
pq.Dispose(); rq.Dispose(); // BEFORE the world (a using-var here would outlive it)in");
world.Dispose();
}
}
}
}
@@ -84,6 +84,8 @@ namespace ProjectM.Tests
public void Purchase_BumpsTier_Withdraws_UpsertsAllClassMembers_FlagsSave()
{
var (world, group) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var dir = MakeDirector(em, RunLifecycle.Staging, 25);
var warriorA = MakePlayer(em, 1, ClassTraits.WarriorClass);
@@ -104,13 +106,15 @@ namespace ProjectM.Tests
Assert.AreEqual(0, rowsR);
Assert.AreEqual(1, em.GetComponentData<SaveRequest>(dir).Pending, "purchase flags the autosave");
Assert.AreEqual(0, PendingRequests(em), "request consumed");
world.Dispose();
}
}
[Test]
public void TwoSameTick_BarelyEnough_ExactlyOneSucceeds()
{
var (world, group) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var dir = MakeDirector(em, RunLifecycle.Staging, 10); // ids 1 and 4 BOTH cost 10 at tier 0
var warrior = MakePlayer(em, 1, ClassTraits.WarriorClass);
@@ -126,13 +130,15 @@ namespace ProjectM.Tests
Assert.AreEqual(0, StorageMath.TotalOf(em.GetBuffer<StorageEntry>(dir, true), ResourceId.Aether),
"the single cost fully drained the ledger — and never went negative (TotalOf pre-check)");
Assert.AreEqual(0, PendingRequests(em), "both requests consumed");
world.Dispose();
}
}
[Test]
public void SecondPurchase_AbsoluteUpsert_SingleRow_RampedCost()
{
var (world, group) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var dir = MakeDirector(em, RunLifecycle.Staging, 30); // tier1 = 10, tier2 = 10 + 1*5 = 15
var warrior = MakePlayer(em, 1, ClassTraits.WarriorClass);
@@ -150,13 +156,15 @@ namespace ProjectM.Tests
Assert.AreEqual(1, rows, "one modifier row — an incremental append would double-count in recompute");
Assert.AreEqual(5, StorageMath.TotalOf(em.GetBuffer<StorageEntry>(dir, true), ResourceId.Aether),
"linear ramp: 30 - 10 - 15");
world.Dispose();
}
}
[Test]
public void Rejects_WrongClassMask_MaxTierCap_NonStaging()
{
var (world, group) = MakeWorld();
using (world)
{
var em = world.EntityManager;
var dir = MakeDirector(em, RunLifecycle.Staging, 999);
var warrior = MakePlayer(em, 1, ClassTraits.WarriorClass);
@@ -184,7 +192,7 @@ namespace ProjectM.Tests
"non-Staging purchase dropped");
Assert.AreEqual(999, StorageMath.TotalOf(em.GetBuffer<StorageEntry>(dir, true), ResourceId.Aether));
Assert.AreEqual(0, PendingRequests(em), "every request consumed, accepted or not");
world.Dispose();
}
}
}
}
@@ -77,6 +77,8 @@ namespace ProjectM.Tests
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);
@@ -92,13 +94,15 @@ namespace ProjectM.Tests
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");
world.Dispose();
}
}
[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);
@@ -111,13 +115,15 @@ namespace ProjectM.Tests
Assert.AreEqual(0, em.GetComponentData<PlayerReady>(player).Value, "mid-run toggle dropped");
Assert.AreEqual(0, PendingRequests(em), "request still consumed");
world.Dispose();
}
}
[Test]
public void PartialReady_DoesNotLaunch()
{
var (world, group, dir) = MakeWorld();
using (world)
{
var em = world.EntityManager;
MakePlayer(em, 1);
MakePlayer(em, 2);
@@ -128,13 +134,15 @@ namespace ProjectM.Tests
Assert.AreEqual(RunLifecycle.Staging, em.GetComponentData<RunInfo>(dir).Lifecycle,
"1/2 ready must NOT launch");
world.Dispose();
}
}
[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);
@@ -149,13 +157,15 @@ namespace ProjectM.Tests
var info = em.GetComponentData<RunInfo>(dir);
Assert.AreEqual(RunLifecycle.Staging, info.Lifecycle, "un-ready aborts the countdown");
Assert.AreEqual(0u, info.LaunchTick, "telegraph cleared");
world.Dispose();
}
}
[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);
@@ -187,13 +197,15 @@ namespace ProjectM.Tests
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);
world.Dispose();
}
}
[Test]
public void LaunchGuard_BlocksWhenOutcomeLatched()
{
var (world, group, dir) = MakeWorld();
using (world)
{
var em = world.EntityManager;
MakePlayer(em, 1);
var conn = MakeConnection(em, 1);
@@ -204,7 +216,7 @@ namespace ProjectM.Tests
Assert.AreEqual(RunLifecycle.Staging, em.GetComponentData<RunInfo>(dir).Lifecycle,
"a decided run must not launch (F2 guard)");
world.Dispose();
}
}
}
}
@@ -92,6 +92,8 @@ namespace ProjectM.Tests
public void Seeds_ByRoomDifficulty_AndSpawnsTaggedAtActiveSlotOrigin()
{
var (world, group, runDir, zoneDir) = MakeWorld(currentRoom: 0, currentNodeId: RunMap.NodeId(0, 0), activeSubSlot: 0);
using (world)
{
var em = world.EntityManager;
var map = RunMapMath.Generate(Seed);
var plan = RoomLayoutMath.Plan(map.NodeAt(RunMap.NodeId(0, 0)), 0, map.LayerCount);
@@ -120,7 +122,7 @@ namespace ProjectM.Tests
Assert.LessOrEqual(math.distance(xfs[0].Position.xz, origin.xz), 14f + 0.01f,
"ring-spawned around the ACTIVE sub-slot origin");
xfs.Dispose(); regs.Dispose(); rooms.Dispose(); q.Dispose(); // dispose BEFORE the worldispose();
world.Dispose();
}
}
[Test]
@@ -129,6 +131,8 @@ namespace ProjectM.Tests
var map = RunMapMath.Generate(Seed);
int bossLayer = map.LayerCount - 1;
var (world, group, runDir, zoneDir) = MakeWorld(bossLayer, map.BossNodeId, activeSubSlot: (byte)(bossLayer & 1));
using (world)
{
var em = world.EntityManager;
group.Update(); // seed tick (the landing grace holds the boss)
@@ -145,13 +149,15 @@ namespace ProjectM.Tests
Assert.AreEqual(50f * Tuning.BossHealthMultiplier, hps[0].Max, 1e-3f, "boss health scaled");
Assert.AreEqual(Tuning.BossScaleMultiplier, xfs[0].Scale, 1e-3f, "boss visual scale bumped");
hps.Dispose(); xfs.Dispose(); q.Dispose(); // dispose BEFORE the world (a using-var here outlives it);
world.Dispose();
}
}
[Test]
public void Objective_ClearedLatch_AndIdleOutsideRooms()
{
var (world, group, runDir, zoneDir) = MakeWorld(0, RunMap.NodeId(0, 0), 0);
using (world)
{
var em = world.EntityManager;
// Fabricate a fully-spawned, fully-dead wave for the CURRENT room epoch.
@@ -166,13 +172,15 @@ namespace ProjectM.Tests
group.Update();
Assert.AreEqual(ExpeditionObjectiveState.Idle, em.GetComponentData<ExpeditionObjective>(runDir).State,
"no active room -> Idle (objective still written above the early-return)");
world.Dispose();
}
}
[Test]
public void MaxAlive_PackFitWaits_WithoutConsumingTheSlot()
{
var (world, group, runDir, zoneDir) = MakeWorld(0, RunMap.NodeId(0, 0), 0, maxAlive: 1);
using (world)
{
var em = world.EntityManager;
// One zone enemy already alive fills the cap.
var blocker = em.CreateEntity(typeof(ZoneEnemyTag));
@@ -184,7 +192,7 @@ namespace ProjectM.Tests
var zs = em.GetComponentData<ZoneEnemyState>(zoneDir);
Assert.AreEqual(1, Alive(em), "cap full -> nothing spawned");
Assert.AreEqual(2, zs.RemainingToSpawn, "the slot WAITS (not consumed) until the pack fits");
world.Dispose();
}
}
}
}
@@ -70,6 +70,8 @@ namespace ProjectM.Tests
public void Spawns_OncePerRoomEpoch_PlanCount_TaggedInShape_ScalePreserved()
{
var (world, group, dir, spawnerE, prefab) = MakeWorld(nodeBudget: 12);
using (world)
{
var em = world.EntityManager;
var map = RunMapMath.Generate(Seed);
var plan = RoomLayoutMath.Plan(map.NodeAt(RunMap.NodeId(0, 0)), 0, map.LayerCount);
@@ -100,13 +102,15 @@ namespace ProjectM.Tests
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);
using (world)
{
var em = world.EntityManager;
group.Update();
@@ -125,13 +129,15 @@ namespace ProjectM.Tests
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);
using (world)
{
var em = world.EntityManager;
group.Update();
Assert.Greater(LiveNodes(em), 0, "room content exists");
@@ -147,7 +153,7 @@ namespace ProjectM.Tests
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();
}
}
}
}
@@ -4,6 +4,7 @@ using ProjectM.Simulation;
using Unity.Core;
using Unity.Entities;
using Unity.NetCode;
using System.Collections.Generic;
namespace ProjectM.Tests
{
@@ -18,6 +19,14 @@ namespace ProjectM.Tests
{
const uint Seed = 999u;
const int GateLayer = 2;
readonly List<World> _worlds = new();
[TearDown]
public void Cleanup()
{
foreach (var w in _worlds) if (w.IsCreated) w.Dispose();
_worlds.Clear();
}
static (World world, SimulationSystemGroup group, Entity dir) MakeGateWorld(byte lifecycle = RunLifecycle.RouteSelect)
{
@@ -69,6 +78,8 @@ namespace ProjectM.Tests
public void ValidPick_Latches_WithTrueServerEpoch()
{
var (world, group, dir) = MakeGateWorld();
using (world)
{
var em = world.EntityManager;
MakePlayer(em, 1, RegionId.Expedition);
SendPick(em, 1, optionIndex: 1, forSeed: (int)Seed, forLayer: GateLayer);
@@ -80,13 +91,15 @@ namespace ProjectM.Tests
Assert.AreEqual(1, cmd.OptionIndex, "the picked option");
Assert.AreEqual(3, cmd.ForRunEpoch, "stamped from the TRUE server epoch, never the client echo");
Assert.AreEqual(0, PendingRequests(em), "request consumed");
world.Dispose();
}
}
[Test]
public void TwoSameTickPicks_FirstWins()
{
var (world, group, dir) = MakeGateWorld();
using (world)
{
var em = world.EntityManager;
MakePlayer(em, 1, RegionId.Expedition);
MakePlayer(em, 2, RegionId.Expedition);
@@ -99,7 +112,7 @@ namespace ProjectM.Tests
Assert.AreEqual(1, cmd.HasPick, "exactly one commit");
Assert.AreEqual(0, cmd.OptionIndex, "the FIRST accepted pick wins (in-place latch, DR-014)");
Assert.AreEqual(0, PendingRequests(em), "both requests consumed");
world.Dispose();
}
}
[Test]
@@ -107,51 +120,53 @@ namespace ProjectM.Tests
{
// Wrong run-identity token (a stale pick from the previous run).
var (w1, g1, d1) = MakeGateWorld();
_worlds.Add(w1);
MakePlayer(w1.EntityManager, 1, RegionId.Expedition);
SendPick(w1.EntityManager, 1, 0, forSeed: (int)Seed + 1, forLayer: GateLayer);
g1.Update();
Assert.AreEqual(0, w1.EntityManager.GetComponentData<RouteCommand>(d1).HasPick, "wrong seed rejected");
Assert.AreEqual(0, PendingRequests(w1.EntityManager));
w1.Dispose();
// Wrong layer (a pick from the previous gate of the SAME run).
var (w2, g2, d2) = MakeGateWorld();
_worlds.Add(w2);
MakePlayer(w2.EntityManager, 1, RegionId.Expedition);
SendPick(w2.EntityManager, 1, 0, (int)Seed, forLayer: GateLayer - 1);
g2.Update();
Assert.AreEqual(0, w2.EntityManager.GetComponentData<RouteCommand>(d2).HasPick, "stale layer rejected");
w2.Dispose();
// Option index out of the published range.
var (w3, g3, d3) = MakeGateWorld();
_worlds.Add(w3);
MakePlayer(w3.EntityManager, 1, RegionId.Expedition);
SendPick(w3.EntityManager, 1, optionIndex: 2, (int)Seed, GateLayer); // count is 2 -> max index 1
g3.Update();
Assert.AreEqual(0, w3.EntityManager.GetComponentData<RouteCommand>(d3).HasPick, "out-of-range rejected");
w3.Dispose();
// Base-region sender (N3): a home-bound joiner cannot commit the party's route.
var (w4, g4, d4) = MakeGateWorld();
_worlds.Add(w4);
MakePlayer(w4.EntityManager, 1, RegionId.Base);
SendPick(w4.EntityManager, 1, 0, (int)Seed, GateLayer);
g4.Update();
Assert.AreEqual(0, w4.EntityManager.GetComponentData<RouteCommand>(d4).HasPick, "base sender rejected (N3)");
w4.Dispose();
// Gate closed (mid-room): the pick is dropped, never queued.
var (w5, g5, d5) = MakeGateWorld(lifecycle: RunLifecycle.InRoom);
_worlds.Add(w5);
MakePlayer(w5.EntityManager, 1, RegionId.Expedition);
SendPick(w5.EntityManager, 1, 0, (int)Seed, GateLayer);
g5.Update();
Assert.AreEqual(0, w5.EntityManager.GetComponentData<RouteCommand>(d5).HasPick, "closed gate rejected");
Assert.AreEqual(0, PendingRequests(w5.EntityManager), "request still consumed");
w5.Dispose();
}
[Test]
public void AlreadyLatched_LaterPickIgnored()
{
var (world, group, dir) = MakeGateWorld();
using (world)
{
var em = world.EntityManager;
MakePlayer(em, 1, RegionId.Expedition);
em.SetComponentData(dir, new RouteCommand { HasPick = 1, OptionIndex = 0, ForRunEpoch = 3, ForLayer = GateLayer });
@@ -161,7 +176,7 @@ namespace ProjectM.Tests
var cmd = em.GetComponentData<RouteCommand>(dir);
Assert.AreEqual(0, cmd.OptionIndex, "an already-latched gate ignores later picks");
world.Dispose();
}
}
}
}
@@ -93,6 +93,8 @@ static int RoomEntities(EntityManager em)
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 });
@@ -132,7 +134,7 @@ static int RoomEntities(EntityManager em)
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)");
world.Dispose();
}
}
[Test]
@@ -140,6 +142,8 @@ static int RoomEntities(EntityManager em)
{
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);
@@ -174,13 +178,15 @@ static int RoomEntities(EntityManager em)
group.Update();
Assert.AreEqual(1, em.GetComponentData<GoalProgress>(dir).Charge, "no double credit (F7)");
Assert.AreEqual(1, em.GetComponentData<MetaCounters>(dir).RunsCompleted);
world.Dispose();
}
}
[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 });
@@ -196,13 +202,15 @@ static int RoomEntities(EntityManager em)
var threat = em.GetComponentData<ThreatState>(dir);
Assert.AreEqual(0, threat.PendingReturns, "no retaliation provoked by an abort");
Assert.AreEqual(0, em.GetComponentData<SaveRequest>(dir).Pending, "no save spam on abort");
world.Dispose();
}
}
[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();
@@ -221,13 +229,15 @@ static int RoomEntities(EntityManager em)
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");
world.Dispose();
}
}
[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();
@@ -242,13 +252,15 @@ static int RoomEntities(EntityManager em)
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");
world.Dispose();
}
}
[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();
@@ -262,7 +274,7 @@ static int RoomEntities(EntityManager em)
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)");
world.Dispose();
}
}
}
}
@@ -55,6 +55,8 @@ namespace ProjectM.Tests
public void StalePendingAtBase_DoesNotHoldGate_AndIsStrippedOnExit()
{
var (world, group, dir) = MakeWorld(out var map);
using (world)
{
var em = world.EntityManager;
em.SetComponentData(dir, new RunInfo
{
@@ -86,13 +88,15 @@ namespace ProjectM.Tests
Assert.AreEqual(0, em.GetComponentData<BoonOffer>(alive).Pending,
"no offer survives the gate");
world.Dispose();
}
}
[Test]
public void Advance_TeleportsOnlyParticipants_LateJoinerStaysAtBase()
{
var (world, group, dir) = MakeWorld(out var map);
using (world)
{
var em = world.EntityManager;
em.SetComponentData(dir, new RunInfo
{
@@ -130,7 +134,7 @@ namespace ProjectM.Tests
Assert.AreEqual(RegionId.Base, em.GetComponentData<RegionTag>(lateJoiner).Region,
"the late joiner stays in the Base relevancy bucket");
world.Dispose();
}
}
}
}