LANTERN purge B6: delete the legacy single-ability path (sockets are THE ability model)
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>
This commit is contained in:
@@ -25,11 +25,8 @@ namespace ProjectM.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AbilityFor_And_Normalize_DefaultToWarrior()
|
||||
public void Normalize_DefaultsToWarrior()
|
||||
{
|
||||
Assert.AreEqual((byte)AbilityId.WarriorCone, ClassTraits.AbilityFor(ClassTraits.WarriorClass));
|
||||
Assert.AreEqual((byte)AbilityId.Primary, ClassTraits.AbilityFor(ClassTraits.RangerClass));
|
||||
Assert.AreEqual((byte)AbilityId.WarriorCone, ClassTraits.AbilityFor(0), "unknown class -> Warrior cone");
|
||||
Assert.AreEqual(ClassTraits.WarriorClass, ClassTraits.Normalize(0));
|
||||
Assert.AreEqual(ClassTraits.WarriorClass, ClassTraits.Normalize(99));
|
||||
Assert.AreEqual(ClassTraits.RangerClass, ClassTraits.Normalize(ClassTraits.RangerClass));
|
||||
|
||||
@@ -10,11 +10,12 @@ namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Plain-Entities EditMode tests for the server-only <see cref="EquipSystem"/>. Seeds a player
|
||||
/// (GhostOwner + PlayerTag + InventorySlot + EquipmentSlot[4 rows] + StatModifier + AbilityRef +
|
||||
/// DefaultAbility), an inline-built ItemDatabase singleton, a mock connection, and an Equip/Unequip RPC.
|
||||
/// Pins: weapon-equip sets AbilityRef + adds the slot-tagged mod + moves the item bag->slot; unequip reverses
|
||||
/// and restores DefaultAbility; equip-over-occupied swaps the old item back; a full-bag swap is rejected with
|
||||
/// no item loss; non-equippable / absent / unresolvable-connection requests no-op (request still consumed);
|
||||
/// (GhostOwner + PlayerTag + InventorySlot + EquipmentSlot[4 rows] + StatModifier), an inline-built
|
||||
/// ItemDatabase singleton, a mock connection, and an Equip/Unequip RPC. Weapons are STAT-STICKS
|
||||
/// (LANTERN purge: the old weapon->AbilityRef grant is deleted; abilities live in the socket kit).
|
||||
/// Pins: weapon-equip adds the slot-tagged mod + moves the item bag->slot; unequip reverses;
|
||||
/// equip-over-occupied swaps the old item back; a full-bag swap is rejected with no item loss;
|
||||
/// non-equippable / absent / unresolvable-connection requests no-op (request still consumed);
|
||||
/// the unequip strip removes ONLY the slot sentinel, leaving foreign-SourceId mods (pickup 0u, upgrade) intact.
|
||||
/// </summary>
|
||||
public class EquipSystemTests
|
||||
@@ -31,13 +32,13 @@ namespace ProjectM.Tests
|
||||
|
||||
static ItemModSpec NoMod() => new ItemModSpec { Target = 255 };
|
||||
|
||||
static ItemDefBlob Mk(ushort id, byte slot, byte ability, ItemModSpec m0)
|
||||
static ItemDefBlob Mk(ushort id, byte slot, ItemModSpec m0)
|
||||
{
|
||||
int stackMax = slot <= EquipSlotId.Tool ? 1 : 999;
|
||||
return new ItemDefBlob
|
||||
{
|
||||
ItemId = id, Category = 0, Tier = 0, StackMax = stackMax,
|
||||
EquipSlot = slot, GrantedAbilityId = ability,
|
||||
EquipSlot = slot,
|
||||
Mod0 = m0, Mod1 = NoMod(), Mod2 = NoMod(), Mod3 = NoMod(),
|
||||
};
|
||||
}
|
||||
@@ -54,10 +55,10 @@ namespace ProjectM.Tests
|
||||
var builder = new BlobBuilder(Allocator.Temp);
|
||||
ref var root = ref builder.ConstructRoot<ItemDatabaseBlob>();
|
||||
var arr = builder.Allocate(ref root.Items, 4);
|
||||
arr[0] = Mk(WeaponA, EquipSlotId.Weapon, (byte)AbilityId.FastLight, new ItemModSpec { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.Flat, Value = 5f });
|
||||
arr[1] = Mk(WeaponB, EquipSlotId.Weapon, (byte)AbilityId.SlowHeavy, new ItemModSpec { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.Flat, Value = 9f });
|
||||
arr[2] = Mk(GearArmor, EquipSlotId.Armor, 0, new ItemModSpec { Target = (byte)StatTarget.MoveSpeed, Op = (byte)ModOp.PercentAdd, Value = 0.1f });
|
||||
arr[3] = Mk(Ore, EquipSlotId.None, 0, NoMod());
|
||||
arr[0] = Mk(WeaponA, EquipSlotId.Weapon, new ItemModSpec { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.Flat, Value = 5f });
|
||||
arr[1] = Mk(WeaponB, EquipSlotId.Weapon, new ItemModSpec { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.Flat, Value = 9f });
|
||||
arr[2] = Mk(GearArmor, EquipSlotId.Armor, new ItemModSpec { Target = (byte)StatTarget.MoveSpeed, Op = (byte)ModOp.PercentAdd, Value = 0.1f });
|
||||
arr[3] = Mk(Ore, EquipSlotId.None, NoMod());
|
||||
_blob = builder.CreateBlobAssetReference<ItemDatabaseBlob>(Allocator.Persistent);
|
||||
builder.Dispose();
|
||||
var dbE = em.CreateEntity(typeof(ItemDatabase));
|
||||
@@ -78,8 +79,6 @@ namespace ProjectM.Tests
|
||||
var e = em.CreateEntity();
|
||||
em.AddComponentData(e, new GhostOwner { NetworkId = networkId });
|
||||
em.AddComponent<PlayerTag>(e);
|
||||
em.AddComponentData(e, new AbilityRef { Id = (byte)AbilityId.Primary });
|
||||
em.AddComponentData(e, new DefaultAbility { Id = (byte)AbilityId.Primary });
|
||||
var bag = em.AddBuffer<InventorySlot>(e);
|
||||
foreach (var it in bagItems) bag.Add(new InventorySlot { ItemId = it.id, Count = it.count });
|
||||
var slots = em.AddBuffer<EquipmentSlot>(e);
|
||||
@@ -102,7 +101,6 @@ namespace ProjectM.Tests
|
||||
em.AddComponentData(e, new ReceiveRpcCommandRequest { SourceConnection = conn });
|
||||
}
|
||||
|
||||
static byte Ability(EntityManager em, Entity p) => em.GetComponentData<AbilityRef>(p).Id;
|
||||
static ushort Slot(EntityManager em, Entity p, byte slot) => em.GetBuffer<EquipmentSlot>(p)[slot].ItemId;
|
||||
static int Bag(EntityManager em, Entity p, ushort id) => InventoryMath.CountOf(em.GetBuffer<InventorySlot>(p), id);
|
||||
static int RequestsLeft(EntityManager em) { using var q = em.CreateEntityQuery(typeof(ReceiveRpcCommandRequest)); return q.CalculateEntityCount(); }
|
||||
@@ -125,7 +123,7 @@ namespace ProjectM.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Equip_Weapon_Sets_Ability_Adds_Mod_Moves_Item()
|
||||
public void Equip_Weapon_Adds_Mod_Moves_Item()
|
||||
{
|
||||
var (world, group) = MakeWorld("EquipWeapon");
|
||||
using (world)
|
||||
@@ -137,7 +135,6 @@ namespace ProjectM.Tests
|
||||
|
||||
group.Update();
|
||||
|
||||
Assert.AreEqual((byte)AbilityId.FastLight, Ability(em, player), "The weapon grants its ability into AbilityRef.");
|
||||
Assert.AreEqual(WeaponA, Slot(em, player, EquipSlotId.Weapon), "The weapon occupies the Weapon slot.");
|
||||
Assert.AreEqual(0, Bag(em, player, WeaponA), "The weapon left the bag.");
|
||||
Assert.AreEqual(1, SlotModCount(em, player, EquipSlotId.Weapon), "The weapon's mod is tagged the weapon-slot sentinel.");
|
||||
@@ -146,7 +143,7 @@ namespace ProjectM.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Unequip_Weapon_Restores_Default_Strips_Mods_Returns_Item()
|
||||
public void Unequip_Weapon_Strips_Mods_Returns_Item()
|
||||
{
|
||||
var (world, group) = MakeWorld("UnequipWeapon");
|
||||
using (world)
|
||||
@@ -160,7 +157,6 @@ namespace ProjectM.Tests
|
||||
MakeUnequip(em, EquipSlotId.Weapon, conn);
|
||||
group.Update();
|
||||
|
||||
Assert.AreEqual((byte)AbilityId.Primary, Ability(em, player), "Unequip restores the DefaultAbility.");
|
||||
Assert.AreEqual(0, Slot(em, player, EquipSlotId.Weapon), "The Weapon slot is empty.");
|
||||
Assert.AreEqual(1, Bag(em, player, WeaponA), "The weapon is back in the bag.");
|
||||
Assert.AreEqual(0, SlotModCount(em, player, EquipSlotId.Weapon), "The weapon's mod is stripped.");
|
||||
@@ -183,7 +179,6 @@ namespace ProjectM.Tests
|
||||
group.Update();
|
||||
|
||||
Assert.AreEqual(WeaponB, Slot(em, player, EquipSlotId.Weapon), "Weapon B now occupies the slot.");
|
||||
Assert.AreEqual((byte)AbilityId.SlowHeavy, Ability(em, player), "AbilityRef swaps to weapon B's ability.");
|
||||
Assert.AreEqual(1, Bag(em, player, WeaponA), "Weapon A swapped back into the bag.");
|
||||
Assert.AreEqual(0, Bag(em, player, WeaponB), "Weapon B left the bag.");
|
||||
Assert.AreEqual(1, SlotModCount(em, player, EquipSlotId.Weapon), "Exactly weapon B's single mod remains (A's stripped).");
|
||||
@@ -231,7 +226,6 @@ namespace ProjectM.Tests
|
||||
|
||||
Assert.AreEqual(0, Slot(em, player, EquipSlotId.Weapon), "Nothing equipped.");
|
||||
Assert.AreEqual(5, Bag(em, player, Ore), "The resource is untouched.");
|
||||
Assert.AreEqual((byte)AbilityId.Primary, Ability(em, player), "AbilityRef unchanged.");
|
||||
Assert.AreEqual(0, RequestsLeft(em), "Both requests are consumed.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,10 @@ namespace ProjectM.Tests
|
||||
|
||||
static Entity MakePlayerPrefab(EntityManager em)
|
||||
{
|
||||
var e = em.CreateEntity(typeof(LocalTransform), typeof(GhostOwner), typeof(AbilityRef), typeof(PlayerTag));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Plain-Entities test for <see cref="StatRecomputeSystem"/>: builds an AbilityDatabase blob singleton
|
||||
/// + a player-like entity (refs / modifier buffer / effective components), ticks the
|
||||
/// SimulationSystemGroup, and asserts the effective stats equal the folded (base + modifiers) values
|
||||
/// and stay stable across repeated ticks (the every-tick recompute is idempotent). Version-independent.
|
||||
/// + a player-like entity (CharacterStatsRef / socket loadout / modifier buffer / effective components),
|
||||
/// ticks the SimulationSystemGroup, and asserts the effective stats equal the folded (base + modifiers)
|
||||
/// values and stay stable across repeated ticks (the every-tick recompute is idempotent). The legacy
|
||||
/// AbilityRef -> EffectiveAbilityStats fold is deleted (LANTERN purge); the socket fold is the ability path.
|
||||
/// </summary>
|
||||
public class StatRecomputeSystemTests
|
||||
{
|
||||
@@ -47,9 +48,8 @@ namespace ProjectM.Tests
|
||||
em.SetComponentData(dbEntity, new AbilityDatabase { Value = blob });
|
||||
|
||||
var player = em.CreateEntity(
|
||||
typeof(AbilityRef), typeof(CharacterStatsRef), typeof(StatModifier),
|
||||
typeof(EffectiveAbilityStats), typeof(EffectiveCharacterStats), typeof(Simulate));
|
||||
em.SetComponentData(player, new AbilityRef { Id = AbilityPrimary });
|
||||
typeof(CharacterStatsRef), typeof(StatModifier),
|
||||
typeof(EffectiveCharacterStats), typeof(Simulate));
|
||||
em.SetComponentData(player, new CharacterStatsRef { Id = CharDefault });
|
||||
return (world, player);
|
||||
}
|
||||
@@ -67,12 +67,9 @@ namespace ProjectM.Tests
|
||||
try
|
||||
{
|
||||
world.GetExistingSystemManaged<SimulationSystemGroup>().Update();
|
||||
var ea = world.EntityManager.GetComponentData<EffectiveAbilityStats>(player);
|
||||
var ec = world.EntityManager.GetComponentData<EffectiveCharacterStats>(player);
|
||||
Assert.AreEqual(20f, ea.Damage, 1e-3f);
|
||||
Assert.AreEqual(25f, ea.ProjectileSpeed, 1e-3f);
|
||||
Assert.AreEqual(12, ea.CooldownTicks);
|
||||
Assert.AreEqual(6f, ec.MoveSpeed, 1e-3f);
|
||||
Assert.AreEqual(12.5f, ec.TurnRateRadiansPerSec, 1e-3f);
|
||||
Assert.AreEqual(100f, ec.MaxHealth, 1e-3f);
|
||||
}
|
||||
finally { world.Dispose(); blob.Dispose(); }
|
||||
@@ -84,14 +81,12 @@ namespace ProjectM.Tests
|
||||
var (world, player) = MakeWorld(out var blob);
|
||||
try
|
||||
{
|
||||
AddMod(world, player, StatTarget.Damage, ModOp.Flat, 5f);
|
||||
AddMod(world, player, StatTarget.Damage, ModOp.PercentAdd, 0.5f); // (20+5)*1.5 = 37.5
|
||||
AddMod(world, player, StatTarget.MoveSpeed, ModOp.PercentAdd, 0.5f); // 6*1.5 = 9
|
||||
AddMod(world, player, StatTarget.MaxHealth, ModOp.Flat, 30f); // 100+30 = 130
|
||||
world.GetExistingSystemManaged<SimulationSystemGroup>().Update();
|
||||
var ea = world.EntityManager.GetComponentData<EffectiveAbilityStats>(player);
|
||||
var ec = world.EntityManager.GetComponentData<EffectiveCharacterStats>(player);
|
||||
Assert.AreEqual(37.5f, ea.Damage, 1e-3f);
|
||||
Assert.AreEqual(9f, ec.MoveSpeed, 1e-3f);
|
||||
Assert.AreEqual(130f, ec.MaxHealth, 1e-3f);
|
||||
}
|
||||
finally { world.Dispose(); blob.Dispose(); }
|
||||
}
|
||||
@@ -102,13 +97,13 @@ namespace ProjectM.Tests
|
||||
var (world, player) = MakeWorld(out var blob);
|
||||
try
|
||||
{
|
||||
AddMod(world, player, StatTarget.Damage, ModOp.Flat, 10f); // 20+10 = 30
|
||||
AddMod(world, player, StatTarget.MaxHealth, ModOp.Flat, 10f); // 100+10 = 110
|
||||
var group = world.GetExistingSystemManaged<SimulationSystemGroup>();
|
||||
group.Update();
|
||||
var first = world.EntityManager.GetComponentData<EffectiveAbilityStats>(player).Damage;
|
||||
var first = world.EntityManager.GetComponentData<EffectiveCharacterStats>(player).MaxHealth;
|
||||
for (int i = 0; i < 5; i++) group.Update();
|
||||
var last = world.EntityManager.GetComponentData<EffectiveAbilityStats>(player).Damage;
|
||||
Assert.AreEqual(30f, first, 1e-3f);
|
||||
var last = world.EntityManager.GetComponentData<EffectiveCharacterStats>(player).MaxHealth;
|
||||
Assert.AreEqual(110f, first, 1e-3f);
|
||||
Assert.AreEqual(first, last, 1e-4f);
|
||||
}
|
||||
finally { world.Dispose(); blob.Dispose(); }
|
||||
@@ -128,13 +123,10 @@ namespace ProjectM.Tests
|
||||
var dbEntity = em.CreateEntity(typeof(AbilityDatabase));
|
||||
em.SetComponentData(dbEntity, new AbilityDatabase { Value = blob });
|
||||
|
||||
// player carrying the socket loadout + per-socket effective buffer (plus the legacy singles the
|
||||
// first fold loop still needs during the step-1b transition).
|
||||
// player carrying the socket loadout + per-socket effective buffer.
|
||||
var player = em.CreateEntity(
|
||||
typeof(AbilityRef), typeof(CharacterStatsRef), typeof(StatModifier),
|
||||
typeof(EffectiveAbilityStats), typeof(EffectiveCharacterStats),
|
||||
typeof(CharacterStatsRef), typeof(StatModifier), typeof(EffectiveCharacterStats),
|
||||
typeof(AbilitySocket), typeof(EffectiveSocketStats), typeof(Simulate));
|
||||
em.SetComponentData(player, new AbilityRef { Id = AbilityPrimary });
|
||||
em.SetComponentData(player, new CharacterStatsRef { Id = CharDefault });
|
||||
var socketBuf = em.GetBuffer<AbilitySocket>(player);
|
||||
var effBuf = em.GetBuffer<EffectiveSocketStats>(player);
|
||||
|
||||
Reference in New Issue
Block a user