4a8220ad3e
AbilityRef, AbilityCooldown, EffectiveAbilityStats, DefaultAbility deleted. GoInGameServerSystem seeds the per-frame 4-socket Spark loadout UNCONDITIONALLY (was gym-only); ClassSelectReceiveSystem + DebugOp.SetClass swap FrameId + re-seed sockets + zero SocketCooldown; ClassSwapUtil.Apply drops newAbilityId; EquipSystem weapons become stat-sticks (GrantedAbilityId removed from the item blob/authoring); StatRecomputeSystem folds CharacterStatsRef + sockets only; HUD cooldown bar reads socket 0 of SocketCooldown/EffectiveSocketStats; class HUD readers are FrameId-only (ClassForAbility/AbilityFor deleted); DebugModifierInjectionSystem drops CycleAbility. 390 tests green; Play-verified: a non-gym spawn gets frame=2 with Sparks [7,8,6,9] and no console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
137 lines
6.7 KiB
C#
137 lines
6.7 KiB
C#
using NUnit.Framework;
|
|
using ProjectM.Server;
|
|
using ProjectM.Simulation;
|
|
using Unity.Collections;
|
|
using Unity.Core;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.NetCode;
|
|
using Unity.Transforms;
|
|
|
|
namespace ProjectM.Tests
|
|
{
|
|
/// <summary>
|
|
/// Pins Step 12a — the born-correct PERMANENT meta seeding in <see cref="GoInGameServerSystem"/>: a spawning
|
|
/// player replays its class's persisted <see cref="MetaTierState"/> tiers as meta-band StatModifiers
|
|
/// (Value = ValuePerTier * tier, SourceId = MetaSourceIdBase + id); other-class rows and unknown ids are
|
|
/// skipped; an over-MaxTier saved row is CLAMPED (D-F5); and the availability guard blocks the WHOLE spawn
|
|
/// (request preserved, nothing consumed) when the catalog is absent (N2).
|
|
/// </summary>
|
|
public class MetaSeedingTests
|
|
{
|
|
static (World world, SimulationSystemGroup group) MakeWorld()
|
|
{
|
|
var world = new World("MetaSeedTest");
|
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<GoInGameServerSystem>());
|
|
group.SortSystems();
|
|
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
|
|
return (world, group);
|
|
}
|
|
|
|
static Entity MakePlayerPrefab(EntityManager em)
|
|
{
|
|
var e = em.CreateEntity(typeof(LocalTransform), typeof(GhostOwner), typeof(FrameId), typeof(PlayerTag));
|
|
em.SetComponentData(e, LocalTransform.Identity);
|
|
em.AddBuffer<StatModifier>(e);
|
|
em.AddBuffer<AbilitySocket>(e); // the spawn path SetBuffers the frame loadout unconditionally (LANTERN purge)
|
|
em.AddComponent<Prefab>(e);
|
|
return e;
|
|
}
|
|
|
|
static void MakeSpawnRequest(EntityManager em, byte classId)
|
|
{
|
|
var conn = em.CreateEntity(typeof(NetworkId));
|
|
em.SetComponentData(conn, new NetworkId { Value = 1 });
|
|
em.AddBuffer<LinkedEntityGroup>(conn);
|
|
var req = em.CreateEntity(typeof(GoInGameRequest), typeof(ReceiveRpcCommandRequest));
|
|
em.SetComponentData(req, new GoInGameRequest { ClassId = classId });
|
|
em.SetComponentData(req, new ReceiveRpcCommandRequest { SourceConnection = conn });
|
|
}
|
|
|
|
static int MetaRows(EntityManager em, Entity player, out float firstValue, out uint firstSource)
|
|
{
|
|
firstValue = 0f; firstSource = 0;
|
|
var mods = em.GetBuffer<StatModifier>(player);
|
|
int n = 0;
|
|
for (int i = 0; i < mods.Length; i++)
|
|
if (mods[i].SourceId >= Tuning.MetaSourceIdBase
|
|
&& mods[i].SourceId < Tuning.MetaSourceIdBase + Tuning.MetaSourceIdSpan)
|
|
{
|
|
if (n == 0) { firstValue = mods[i].Value; firstSource = mods[i].SourceId; }
|
|
n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
[Test]
|
|
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));
|
|
em.SetComponentData(spawnerE, new PlayerSpawner { PlayerPrefab = prefab, SpawnRingRadius = 2f, RingSlots = 8 });
|
|
|
|
var catalogE = em.CreateEntity(typeof(MetaUpgradeCatalog));
|
|
em.SetComponentData(catalogE, new MetaUpgradeCatalog { Value = MetaCatalogData.BuildDefault() });
|
|
var record = em.AddBuffer<MetaTierState>(catalogE); // the tier record rides any singleton entity in tests
|
|
byte warrior = ClassTraits.WarriorClass; // normalized CharacterId (2)
|
|
record.Add(new MetaTierState { ClassId = warrior, UpgradeId = 1, Tier = 2 }); // Reinforced Frame t2 -> +30
|
|
record.Add(new MetaTierState { ClassId = warrior, UpgradeId = 5, Tier = 9 }); // Warrior's Might, saved OVER MaxTier(4) -> clamp
|
|
record.Add(new MetaTierState { ClassId = warrior, UpgradeId = 200, Tier = 1 }); // unknown id -> skipped
|
|
record.Add(new MetaTierState { ClassId = ClassTraits.RangerClass, UpgradeId = 2, Tier = 3 }); // other class -> skipped
|
|
record.Add(new MetaTierState { ClassId = warrior, UpgradeId = 7, Tier = 1 }); // Ranger-masked (Longshot) -> skipped
|
|
|
|
MakeSpawnRequest(em, warrior);
|
|
group.Update();
|
|
|
|
var pq = em.CreateEntityQuery(typeof(PlayerTag), typeof(PlayerClass));
|
|
var players = pq.ToEntityArray(Allocator.Temp);
|
|
Assert.AreEqual(1, players.Length, "player spawned");
|
|
var player = players[0];
|
|
players.Dispose(); pq.Dispose(); // BEFORE the world
|
|
|
|
var mods = em.GetBuffer<StatModifier>(player);
|
|
float frameValue = 0f, mightValue = 0f;
|
|
int metaRows = 0;
|
|
for (int i = 0; i < mods.Length; i++)
|
|
{
|
|
if (mods[i].SourceId == Tuning.MetaSourceIdBase + 1) { frameValue = mods[i].Value; metaRows++; }
|
|
else if (mods[i].SourceId == Tuning.MetaSourceIdBase + 5) { mightValue = mods[i].Value; metaRows++; }
|
|
else if (mods[i].SourceId >= Tuning.MetaSourceIdBase
|
|
&& mods[i].SourceId < Tuning.MetaSourceIdBase + Tuning.MetaSourceIdSpan) metaRows++;
|
|
}
|
|
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)");
|
|
}
|
|
}
|
|
|
|
[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));
|
|
em.SetComponentData(spawnerE, new PlayerSpawner { PlayerPrefab = prefab, SpawnRingRadius = 2f, RingSlots = 8 });
|
|
// NO catalog, NO tier record.
|
|
MakeSpawnRequest(em, ClassTraits.WarriorClass);
|
|
|
|
group.Update();
|
|
|
|
var pq = em.CreateEntityQuery(typeof(PlayerClass));
|
|
Assert.AreEqual(0, pq.CalculateEntityCount(), "no player spawned while blocked (N2)");
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
}
|