diff --git a/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs b/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs index 272b25298..42234f80d 100644 --- a/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs @@ -41,7 +41,6 @@ namespace ProjectM.Authoring Tier = def.Tier, StackMax = def.StackMax, EquipSlot = def.EquipSlot, - GrantedAbilityId = def.GrantedAbilityId, Mod0 = ModAt(def, 0), Mod1 = ModAt(def, 1), Mod2 = ModAt(def, 2), diff --git a/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs b/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs index d5da0d779..358e3a794 100644 --- a/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs +++ b/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs @@ -33,9 +33,6 @@ namespace ProjectM.Authoring [Tooltip("EquipSlotId byte: 0=Weapon, 1=Armor, 2=Trinket, 3=Tool, 255=not equippable.")] public byte EquipSlot = 255; - [Tooltip("AbilityId granted when equipped in the Weapon slot (0=none): 1=Primary, 2=FastLight, 3=SlowHeavy.")] - public byte GrantedAbilityId = 0; - [Tooltip("Stat modifiers granted while equipped (first 4 used).")] public List Mods = new List(); } diff --git a/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs b/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs index 4c75c3892..e5c079aa9 100644 --- a/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs @@ -19,9 +19,6 @@ namespace ProjectM.Authoring [Tooltip("Character-stats definition (move speed, turn rate, max health). Single source of those values.")] public CharacterStatsDefinition Character; - [Tooltip("Ability definition occupying the player's primary slot.")] - public AbilityDefinition PrimaryAbility; - [Header("Fallbacks (used only if a definition above is unassigned)")] [Min(0f)] public float FallbackMaxHealth = 100f; @@ -42,14 +39,11 @@ namespace ProjectM.Authoring { var entity = GetEntity(authoring, TransformUsageFlags.Dynamic); - // Re-bake when a referenced definition's serialized values change. + // Re-bake when the referenced definition's serialized values change. if (authoring.Character != null) DependsOn(authoring.Character); - if (authoring.PrimaryAbility != null) DependsOn(authoring.PrimaryAbility); byte characterId = authoring.Character != null ? (byte)authoring.Character.Id : (byte)CharacterId.Default; - byte abilityId = authoring.PrimaryAbility != null - ? (byte)authoring.PrimaryAbility.Id : (byte)AbilityId.Primary; float maxHealth = authoring.Character != null ? authoring.Character.MaxHealth : authoring.FallbackMaxHealth; @@ -57,14 +51,11 @@ namespace ProjectM.Authoring AddComponent(entity); AddComponent(entity); - // Data-driven stat refs (replace M2's inlined PlayerMoveStats / AbilityStats values). + // Data-driven stat ref (replaces M2's inlined PlayerMoveStats values); the ability model is the + // 4-socket kit below (the legacy AbilityRef/DefaultAbility/EffectiveAbilityStats bakes are deleted). AddComponent(entity, new CharacterStatsRef { Id = characterId }); - AddComponent(entity, new AbilityRef { Id = abilityId }); - // Unarmed/base ability restored on weapon-unequip (AbilityRef.Id mutates when a weapon is equipped). - AddComponent(entity, new DefaultAbility { Id = abilityId }); // Effective stats: zeroed at bake, recomputed every predicted tick by StatRecomputeSystem. - AddComponent(entity, new EffectiveAbilityStats()); AddComponent(entity, new EffectiveCharacterStats()); // Empty replicated modifier stack (grown by upgrades/pickups/debug hook, server-authoritative). @@ -82,7 +73,6 @@ namespace ProjectM.Authoring // damageable hit radius, predicted cooldown state, and the per-tick damage inbox. AddComponent(entity, new Health { Current = maxHealth, Max = maxHealth }); AddComponent(entity, new HitRadius { Value = authoring.HitRadius }); - AddComponent(entity); AddBuffer(entity); // MC-1 dash: predicted dash window (derived from PlayerInput.Dash) + cooldown gate, baked idle/ready. AddComponent(entity); @@ -110,17 +100,17 @@ namespace ProjectM.Authoring // the Returning edge) + the server-only Blade-Dash per-dash dedup accumulator (non-replicated). AddComponent(entity); AddComponent(entity); - // LANTERN Phase 1 (Step 1): 4-socket kit data model - parallels AbilityRef/AbilityCooldown - // (both kept until the AbilityFireSystem migration in Step 2). AbilitySocket = cold per-socket - // loadout (EquipmentSlot-modelled, 4 empty rows); SocketCooldown = hot owner-predicted per-socket - // cooldown; FrameId = replicated frame/class signal (baked 0, written server-side at frame select). + // LANTERN Phase 1 (Step 1): 4-socket kit data model — THE ability model (the legacy single + // AbilityRef/AbilityCooldown path is deleted). AbilitySocket = cold per-socket loadout + // (EquipmentSlot-modelled, 4 empty rows; GoInGameServerSystem seeds the frame loadout at spawn); + // SocketCooldown = hot owner-predicted per-socket cooldown; FrameId = replicated frame/class signal + // (baked 0, written server-side at frame select). var sockets = AddBuffer(entity); for (int sk = 0; sk < SocketId.Count; sk++) sockets.Add(new AbilitySocket { SparkId = 0 }); AddComponent(entity); AddComponent(entity); - // Step 1b: per-socket effective-stats buffer (4 rows), folded each predicted tick by - // StatRecomputeSystem (additive; the legacy single EffectiveAbilityStats stays until steps 2/2.5). + // Step 1b: per-socket effective-stats buffer (4 rows), folded each predicted tick by StatRecomputeSystem. var effSockets = AddBuffer(entity); for (int sk2 = 0; sk2 < SocketId.Count; sk2++) effSockets.Add(new EffectiveSocketStats()); diff --git a/Assets/_Project/Scripts/Client/Presentation/ClassPrepPortalHudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/ClassPrepPortalHudSystem.cs index a2c7ff471..a3c5f33d1 100644 --- a/Assets/_Project/Scripts/Client/Presentation/ClassPrepPortalHudSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/ClassPrepPortalHudSystem.cs @@ -81,9 +81,9 @@ namespace ProjectM.Client // Local class from the replicated AbilityRef (tracks the dev class-switch; PlayerClass is server-only). byte localClass = ClassTraits.WarriorClass; bool haveLocalPlayer = false; - foreach (var (fr, ar) in SystemAPI.Query, RefRO>().WithAll()) + foreach (var fr in SystemAPI.Query>().WithAll()) { - localClass = fr.ValueRO.Value != 0 ? fr.ValueRO.Value : ClassTraits.ClassForAbility(ar.ValueRO.Id); // FrameId signal, AbilityRef fallback + localClass = ClassTraits.Normalize(fr.ValueRO.Value); // FrameId is the sole class signal (legacy AbilityRef deleted) haveLocalPlayer = true; break; } diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs index 7ca3b1fe8..6fd8c0ac1 100644 --- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs @@ -134,8 +134,8 @@ namespace ProjectM.Client // stays correct the day a Health/stats writer is parallelised. EntityManager.CompleteDependencyBeforeRO(); EntityManager.CompleteDependencyBeforeRO(); - EntityManager.CompleteDependencyBeforeRO(); - EntityManager.CompleteDependencyBeforeRO(); + EntityManager.CompleteDependencyBeforeRO(); + EntityManager.CompleteDependencyBeforeRO(); EntityManager.CompleteDependencyBeforeRO(); float dt = SystemAPI.Time.DeltaTime; // wall-frame delta — correct in a presentation system @@ -330,9 +330,9 @@ namespace ProjectM.Client float hp = 0f, maxHp = 1f, cdFrac = 1f; bool dead = false, shielded = false; - foreach (var (health, effChar, effAbility, cd, invuln, entity) in - SystemAPI.Query, RefRO, RefRO, - RefRO, RefRO>() + foreach (var (health, effChar, cd, invuln, entity) in + SystemAPI.Query, RefRO, + RefRO, RefRO>() .WithAll().WithEntityAccess()) { found = true; @@ -340,8 +340,15 @@ namespace ProjectM.Client maxHp = effChar.ValueRO.MaxHealth > 0f ? effChar.ValueRO.MaxHealth : health.ValueRO.Max; dead = SystemAPI.IsComponentEnabled(entity); - uint nextFire = cd.ValueRO.NextFireTick; - int cdTicks = effAbility.ValueRO.CooldownTicks; + // Cooldown bar = socket 0 (the primary Spark) of the 4-socket kit (the legacy single + // AbilityCooldown died — LANTERN purge). EffectiveSocketStats row 0 supplies the duration. + uint nextFire = cd.ValueRO.Get(0); + int cdTicks = 0; + if (SystemAPI.HasBuffer(entity)) + { + var effSockets = SystemAPI.GetBuffer(entity); + if (effSockets.Length > 0) cdTicks = effSockets[0].CooldownTicks; + } var nextTick = new NetworkTick(nextFire); cdFrac = (haveTick && nextFire != 0 && cdTicks > 0 && nextTick.IsValid && nextTick.IsNewerThan(nt.ServerTick)) ? Mathf.Clamp01(1f - nextTick.TicksSince(nt.ServerTick) / (float)cdTicks) diff --git a/Assets/_Project/Scripts/Client/Presentation/MetaShopHudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/MetaShopHudSystem.cs index 57a2edab5..32de8a67c 100644 --- a/Assets/_Project/Scripts/Client/Presentation/MetaShopHudSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/MetaShopHudSystem.cs @@ -74,9 +74,9 @@ namespace ProjectM.Client // server-only); tiers from the replicated MetaTierState record on the director ghost. byte localClass = ClassTraits.WarriorClass; bool haveLocalPlayer = false; - foreach (var (fr, ar) in SystemAPI.Query, RefRO>().WithAll()) + foreach (var fr in SystemAPI.Query>().WithAll()) { - localClass = fr.ValueRO.Value != 0 ? fr.ValueRO.Value : ClassTraits.ClassForAbility(ar.ValueRO.Id); // FrameId signal, AbilityRef fallback + localClass = ClassTraits.Normalize(fr.ValueRO.Value); // FrameId is the sole class signal (legacy AbilityRef deleted) haveLocalPlayer = true; break; } diff --git a/Assets/_Project/Scripts/Server/Combat/ClassSelectReceiveSystem.cs b/Assets/_Project/Scripts/Server/Combat/ClassSelectReceiveSystem.cs index 4045fd969..dfc27be4a 100644 --- a/Assets/_Project/Scripts/Server/Combat/ClassSelectReceiveSystem.cs +++ b/Assets/_Project/Scripts/Server/Combat/ClassSelectReceiveSystem.cs @@ -6,12 +6,13 @@ using Unity.NetCode; namespace ProjectM.Server { /// - /// Server receiver for — the player picks their class at base. Honored ONLY in - /// Staging (class = a between-runs choice; mid-run it would desync the fight). Resolves sender → player (the + /// Server receiver for — the player picks their frame at base. Honored ONLY in + /// Staging (frame = a between-runs choice; mid-run it would desync the fight). Resolves sender → player (the /// MetaSpend/ReadyToggle idiom), then applies the FULL in-place swap via (class seeds + - /// permanent-meta re-sync) and writes AbilityRef / PlayerClass / AbilityCooldown + . - /// Plain server group, before RunDirectorSystem (the receiver convention); requests are ALWAYS destroyed. NOT - /// Burst-compiled (a cross-assembly blob+buffer helper on a low-frequency RPC — Burst safety over micro-perf). + /// permanent-meta re-sync), writes FrameId / PlayerClass, re-seeds the 4-socket Spark loadout, and calls + /// . Plain server group, before RunDirectorSystem (the receiver convention); + /// requests are ALWAYS destroyed. NOT Burst-compiled (a cross-assembly blob+buffer helper on a low-frequency RPC). + /// /// [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] @@ -50,18 +51,26 @@ namespace ProjectM.Server var conn = receive.ValueRO.SourceConnection; if (!PlayerResolve.TryResolve(ref state, playerByConn, conn, out var player)) continue; - if (!SystemAPI.HasComponent(player)) continue; + if (!SystemAPI.HasBuffer(player)) continue; var mods = SystemAPI.GetBuffer(player); var metaRecord = haveMeta ? SystemAPI.GetBuffer(dir) : default; - ClassSwapUtil.Apply(req.ValueRO.ClassId, mods, haveMeta, metaCat, metaRecord, - out byte newClass, out byte newAbilityId); + ClassSwapUtil.Apply(req.ValueRO.ClassId, mods, haveMeta, metaCat, metaRecord, out byte newClass); - SystemAPI.SetComponent(player, new AbilityRef { Id = newAbilityId }); + if (SystemAPI.HasComponent(player)) + SystemAPI.SetComponent(player, new FrameId { Value = newClass }); if (SystemAPI.HasComponent(player)) SystemAPI.SetComponent(player, new PlayerClass { ClassId = newClass }); - if (SystemAPI.HasComponent(player)) - SystemAPI.SetComponent(player, new AbilityCooldown { NextFireTick = 0 }); // swapped ability fires now + // Re-seed the 4-socket Spark loadout for the new frame + clear its cooldowns (fires now). + ClassTraits.FrameLoadout(newClass, out byte f0, out byte f1, out byte f2, out byte f3); + var sockets = SystemAPI.GetBuffer(player); + sockets.Clear(); + sockets.Add(new AbilitySocket { SparkId = f0 }); + sockets.Add(new AbilitySocket { SparkId = f1 }); + sockets.Add(new AbilitySocket { SparkId = f2 }); + sockets.Add(new AbilitySocket { SparkId = f3 }); + if (SystemAPI.HasComponent(player)) + SystemAPI.SetComponent(player, default(SocketCooldown)); // 0 = ready: the swapped kit fires now if (haveDb && SystemAPI.HasComponent(player) && SystemAPI.HasComponent(player)) { byte charId = SystemAPI.GetComponent(player).Id; diff --git a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs index ce9eb756d..79c159318 100644 --- a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs +++ b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs @@ -79,26 +79,24 @@ namespace ProjectM.Server ecb.SetComponent(player, new GhostOwner { NetworkId = networkId.Value }); // Tag the player into the base region (M6 region/relevancy split). ecb.AddComponent(player, new RegionTag { Region = RegionId.Base }); - // Slice 2: seed the chosen class on the just-instantiated player. AbilityRef selects the Fire slot - // (Warrior = cone / Ranger = projectile); the DRG-asymmetry traits ride permanent StatModifiers - // (CharacterStatsRef stays Default -> deltas replicate via the OwnerSendType.All buffer). 0 -> Warrior. + // Slice 2 -> LANTERN: seed the chosen frame on the just-instantiated player. The 4-socket Spark + // loadout IS the ability model (the legacy single-AbilityRef path is deleted); the DRG-asymmetry + // traits ride permanent StatModifiers (CharacterStatsRef stays Default -> deltas replicate via the + // OwnerSendType.All buffer). 0 -> Warrior/Bathynaut. byte classId = ClassTraits.Normalize(goReq.ValueRO.ClassId); - ecb.SetComponent(player, new AbilityRef { Id = ClassTraits.AbilityFor(classId) }); ClassTraits.AppendSeeds(classId, player, ecb); // Expedition redesign: the server-only class anchor the meta systems key on (born-correct meta // seeding at Step 12a + per-class spend at Step 13 resolve the tier record through this). ecb.AddComponent(player, new PlayerClass { ClassId = classId }); - ecb.AddComponent(player, new FrameId { Value = classId }); // Add (not Set): baked on the real player; absent on the minimal test prefab // replicated frame/class signal (HUD reads this, not AbilityRef) - if (isGym) - { - // GYM: per-frame default Spark loadout on keys 1-4 (AbilityFireSystem reads sockets, not AbilityRef). - ClassTraits.FrameLoadout(classId, out byte f0, out byte f1, out byte f2, out byte f3); - var gymSockets = ecb.SetBuffer(player); - gymSockets.Add(new AbilitySocket { SparkId = f0 }); - gymSockets.Add(new AbilitySocket { SparkId = f1 }); - gymSockets.Add(new AbilitySocket { SparkId = f2 }); - gymSockets.Add(new AbilitySocket { SparkId = f3 }); - } + ecb.AddComponent(player, new FrameId { Value = classId }); // Add (not Set): baked on the real player; absent on the minimal test prefab // replicated frame/class signal + // Per-frame default Spark loadout on keys 1-4 (UNCONDITIONAL since the legacy path died — without + // this a non-gym spawn would have four empty sockets and no abilities at all). + ClassTraits.FrameLoadout(classId, out byte f0, out byte f1, out byte f2, out byte f3); + var sockets = ecb.SetBuffer(player); + sockets.Add(new AbilitySocket { SparkId = f0 }); + sockets.Add(new AbilitySocket { SparkId = f1 }); + sockets.Add(new AbilitySocket { SparkId = f2 }); + sockets.Add(new AbilitySocket { SparkId = f3 }); // Step 12a: born-correct PERMANENT meta seeding — replay this class's persisted tiers as // meta-band StatModifiers on the just-instantiated player (same ECB as Instantiate, the // ClassTraits idiom). Skip tier 0 / unknown ids (preserve-don't-crash); CLAMP a saved tier above a diff --git a/Assets/_Project/Scripts/Server/Debug/DebugCommandReceiveSystem.cs b/Assets/_Project/Scripts/Server/Debug/DebugCommandReceiveSystem.cs index 543add7d6..d73c71a4b 100644 --- a/Assets/_Project/Scripts/Server/Debug/DebugCommandReceiveSystem.cs +++ b/Assets/_Project/Scripts/Server/Debug/DebugCommandReceiveSystem.cs @@ -142,13 +142,13 @@ namespace ProjectM.Server } break; case DebugOp.SetClass: - // Swap an already-spawned player's class IN PLACE (editor dev tool). Class = two replicated - // pieces: the AbilityRef Fire slot + the ClassSourceId-tagged StatModifier seeds; the owner's - // StatRecomputeSystem refolds EffectiveCharacterStats. Server-authoritative + prediction-correct - // (same buffer-mutation path as GrantUpgrade). Reapply + AbilityRef run unconditionally so the - // class is correct even on a corpse; the heal is gated on a LIVING player so we don't resurrect - // it out-of-band and race PlayerRespawnSystem (which refills to the new max on respawn itself). - if (sender != Entity.Null && SystemAPI.HasComponent(sender) + // Swap an already-spawned player's frame IN PLACE (editor dev tool). Frame = FrameId + the + // ClassSourceId-tagged StatModifier seeds + the 4-socket Spark loadout; the owner's + // StatRecomputeSystem refolds EffectiveCharacterStats. Server-authoritative + prediction- + // correct (same buffer-mutation path as GrantUpgrade). The swap runs even on a corpse; the + // heal is gated on a LIVING player so we don't resurrect out-of-band and race + // PlayerRespawnSystem (which refills to the new max on respawn itself). + if (sender != Entity.Null && SystemAPI.HasBuffer(sender) && SystemAPI.HasBuffer(sender)) { var classMods = SystemAPI.GetBuffer(sender); @@ -156,15 +156,23 @@ namespace ProjectM.Server bool haveMeta2 = SystemAPI.TryGetSingleton(out var metaCat2) && SystemAPI.TryGetSingletonEntity(out dir2) && SystemAPI.HasBuffer(dir2); var metaRec2 = haveMeta2 ? SystemAPI.GetBuffer(dir2) : default; - // DR-046: the FULL swap (class seeds + meta re-sync) now lives in the shared ClassSwapUtil, + // DR-046: the FULL swap (class seeds + meta re-sync) lives in the shared ClassSwapUtil, // used by BOTH this dev path and the base ClassSelectReceiveSystem so they cannot drift. ClassSwapUtil.Apply((byte)cmd.ArgA, classMods, haveMeta2, metaCat2, metaRec2, - out byte swNewClass, out byte swNewAbility); - SystemAPI.SetComponent(sender, new AbilityRef { Id = swNewAbility }); + out byte swNewClass); + if (SystemAPI.HasComponent(sender)) + SystemAPI.SetComponent(sender, new FrameId { Value = swNewClass }); if (SystemAPI.HasComponent(sender)) SystemAPI.SetComponent(sender, new PlayerClass { ClassId = swNewClass }); - if (SystemAPI.HasComponent(sender)) - SystemAPI.SetComponent(sender, new AbilityCooldown { NextFireTick = 0 }); + ClassTraits.FrameLoadout(swNewClass, out byte sf0, out byte sf1, out byte sf2, out byte sf3); + var swSockets = SystemAPI.GetBuffer(sender); + swSockets.Clear(); + swSockets.Add(new AbilitySocket { SparkId = sf0 }); + swSockets.Add(new AbilitySocket { SparkId = sf1 }); + swSockets.Add(new AbilitySocket { SparkId = sf2 }); + swSockets.Add(new AbilitySocket { SparkId = sf3 }); + if (SystemAPI.HasComponent(sender)) + SystemAPI.SetComponent(sender, default(SocketCooldown)); if (SystemAPI.HasComponent(sender) && SystemAPI.HasComponent(sender) && SystemAPI.TryGetSingleton(out var abilityDb2)) { @@ -178,6 +186,7 @@ namespace ProjectM.Server } } break; + break; case DebugOp.SpawnEnemy: // GYM: spawn a chosen enemy KIND (Drowner/Grindylow) from the baked roster near the sender. if (sender != Entity.Null && SystemAPI.HasComponent(sender) diff --git a/Assets/_Project/Scripts/Server/Debug/DebugModifierInjectionSystem.cs b/Assets/_Project/Scripts/Server/Debug/DebugModifierInjectionSystem.cs index 3ffa72eb5..b3a56c0c2 100644 --- a/Assets/_Project/Scripts/Server/Debug/DebugModifierInjectionSystem.cs +++ b/Assets/_Project/Scripts/Server/Debug/DebugModifierInjectionSystem.cs @@ -14,7 +14,6 @@ namespace ProjectM.Server /// client. In-editor single-process only (client + server worlds in one process). Poke from execute_code: /// DebugModifierInjectionSystem.AddModifier((byte)StatTarget.Damage, (byte)ModOp.Flat, 50f); /// DebugModifierInjectionSystem.AddModifier((byte)StatTarget.MoveSpeed, (byte)ModOp.PercentAdd, 0.5f); - /// DebugModifierInjectionSystem.CycleAbility(); // Primary -> FastLight -> SlowHeavy -> Primary /// DebugModifierInjectionSystem.ClearModifiers(); /// All applied to the first player on the next server tick. /// @@ -25,7 +24,6 @@ namespace ProjectM.Server static readonly List s_Pending = new List(); static bool s_Clear; - static bool s_Cycle; /// Queue a modifier to append to the first player on the next server tick. public static void AddModifier(byte target, byte op, float value) @@ -36,17 +34,15 @@ namespace ProjectM.Server /// Clear the first player's whole modifier stack on the next server tick. public static void ClearModifiers() => s_Clear = true; - /// Cycle the first player's primary ability id on the next server tick. - public static void CycleAbility() => s_Cycle = true; protected override void OnUpdate() { - if (s_Pending.Count == 0 && !s_Clear && !s_Cycle) + if (s_Pending.Count == 0 && !s_Clear) return; Entity player = Entity.Null; - foreach (var (abilityRef, e) in - SystemAPI.Query>().WithAll().WithEntityAccess()) + foreach (var (tag, e) in + SystemAPI.Query>().WithAll().WithEntityAccess()) { player = e; break; @@ -70,19 +66,6 @@ namespace ProjectM.Server } s_Pending.Clear(); } - - if (s_Cycle) - { - var abilityRef = EntityManager.GetComponentData(player); - abilityRef.Id = abilityRef.Id switch - { - (byte)AbilityId.Primary => (byte)AbilityId.FastLight, - (byte)AbilityId.FastLight => (byte)AbilityId.SlowHeavy, - _ => (byte)AbilityId.Primary, - }; - EntityManager.SetComponentData(player, abilityRef); - s_Cycle = false; - } } } } diff --git a/Assets/_Project/Scripts/Server/Economy/EquipSystem.cs b/Assets/_Project/Scripts/Server/Economy/EquipSystem.cs index 79bf28e4f..0e73a8d14 100644 --- a/Assets/_Project/Scripts/Server/Economy/EquipSystem.cs +++ b/Assets/_Project/Scripts/Server/Economy/EquipSystem.cs @@ -11,14 +11,14 @@ namespace ProjectM.Server /// Resolves the sender's player (SourceConnection -> NetworkId -> GhostOwner, the AbilityUpgradeSystem / /// InventoryDepositSystem owner-map idiom) and applies the change IN-PLACE: moves the item between the /// personal bag and the loadout (buffer index = slot), - /// sets .Id from the Weapon slot (restoring on - /// weapon-unequip), and adds/strips the item's inline stat mods as s tagged by a + /// and adds/strips the item's inline stat mods as s tagged by a /// per-slot SourceId (Tuning.EquipSourceIdBase + slot), stripped TARGET-AGNOSTICALLY via - /// . + /// . (LANTERN purge: weapons are stat-sticks — the old + /// weapon->ability grant is deleted; abilities live in the 4-socket Spark loadout.) /// - /// Effects are EVENT-DRIVEN (applied once here): AbilityRef + StatModifier are [GhostField]s re-folded by the + /// Effects are EVENT-DRIVEN (applied once here): StatModifier is a [GhostField] buffer re-folded by the /// predicted StatRecomputeSystem every tick and replicated to the owner, so the swap is prediction-correct - /// (DebugModifierInjectionSystem.CycleAbility is the precedent) and survives respawn (the entity persists). + /// and survives respawn (the entity persists). /// Atomicity: an equip into an occupied slot verifies the bag can hold the swapped-out item BEFORE any /// withdrawal and rejects otherwise — no item loss (the co-op-placement commit-in-place rule). Plain server /// SimulationSystemGroup (NOT predicted -> applied once, no rollback double-apply); only the request entity @@ -127,10 +127,8 @@ namespace ProjectM.Server static void ApplySlotEffects(ref SystemState state, Entity player, byte slot, ItemDefBlob def) { - // Weapon slot drives the active ability (swaps prefab + base stats via StatRecomputeSystem). - if (slot == EquipSlotId.Weapon && def.GrantedAbilityId != 0) - state.EntityManager.SetComponentData(player, new AbilityRef { Id = def.GrantedAbilityId }); - + // LANTERN purge: weapons are stat-sticks — the old weapon->AbilityRef ability grant is deleted + // (abilities live in the 4-socket Spark loadout). var mods = state.EntityManager.GetBuffer(player); uint sourceId = Tuning.EquipSourceIdBase + (uint)slot; for (int i = 0; i < ItemDefBlob.MaxMods; i++) @@ -145,13 +143,6 @@ namespace ProjectM.Server { var mods = state.EntityManager.GetBuffer(player); TimedModifierUtil.RemoveBySourceId(mods, Tuning.EquipSourceIdBase + (uint)slot); - - // Weapon slot: restore the unarmed/base ability. - if (slot == EquipSlotId.Weapon) - { - byte fallback = state.EntityManager.GetComponentData(player).Id; - state.EntityManager.SetComponentData(player, new AbilityRef { Id = fallback }); - } } static int StackMaxOf(ref ItemDatabaseBlob db, ushort itemId) diff --git a/Assets/_Project/Scripts/Simulation/Combat/AbilityCooldown.cs b/Assets/_Project/Scripts/Simulation/Combat/AbilityCooldown.cs deleted file mode 100644 index 079b48a80..000000000 --- a/Assets/_Project/Scripts/Simulation/Combat/AbilityCooldown.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Unity.Entities; -using Unity.NetCode; - -namespace ProjectM.Simulation -{ - /// - /// Predicted per-player ability cooldown gate. Holds the earliest server tick at which the - /// owning player may fire again, so can throttle shots - /// deterministically across client prediction and server simulation. - /// - /// Replicated as a so the cooldown survives the frame→tick→rollback - /// boundary: when the client re-predicts ticks after a snapshot, it sees the same authoritative - /// gate the server applied and converges without double-firing. Stored as a raw uint - /// rather than a for simple, quantization-free serialization; compare - /// by wrapping it back into a and using - /// (raw subtraction is unsafe across tick wraparound). - /// - /// - public struct AbilityCooldown : IComponentData - { - /// - /// Raw tick value of the earliest tick the player may fire again. 0 = ready (no - /// cooldown pending). Set by to - /// serverTick + max(1, CooldownTicks) on fire; treat as "still cooling down" only - /// while a valid built from it is newer than the current - /// ServerTick. - /// - [GhostField] public uint NextFireTick; - } -} diff --git a/Assets/_Project/Scripts/Simulation/Combat/AbilityCooldown.cs.meta b/Assets/_Project/Scripts/Simulation/Combat/AbilityCooldown.cs.meta deleted file mode 100644 index aa6be13e4..000000000 --- a/Assets/_Project/Scripts/Simulation/Combat/AbilityCooldown.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b7a2b67b22b2a4abaa8efd84759445c0 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Combat/AbilityRef.cs b/Assets/_Project/Scripts/Simulation/Combat/AbilityRef.cs deleted file mode 100644 index 7a490f113..000000000 --- a/Assets/_Project/Scripts/Simulation/Combat/AbilityRef.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Unity.Entities; -using Unity.NetCode; - -namespace ProjectM.Simulation -{ - /// - /// Which authored ability definition occupies this entity's primary slot - a light replicated key - /// into the AbilityDatabase blob, replacing M2's inlined AbilityStats values. Replicated so an - /// ability swap is server-authoritative and prediction-correct. Id stores an . - /// - public struct AbilityRef : IComponentData - { - [GhostField] public byte Id; - } -} diff --git a/Assets/_Project/Scripts/Simulation/Combat/AbilityRef.cs.meta b/Assets/_Project/Scripts/Simulation/Combat/AbilityRef.cs.meta deleted file mode 100644 index 34bd21c61..000000000 --- a/Assets/_Project/Scripts/Simulation/Combat/AbilityRef.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d6ea08a11ef3d4afdb722b735ca3ed03 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Combat/ClassSwapUtil.cs b/Assets/_Project/Scripts/Simulation/Combat/ClassSwapUtil.cs index 67dd27353..485e8876c 100644 --- a/Assets/_Project/Scripts/Simulation/Combat/ClassSwapUtil.cs +++ b/Assets/_Project/Scripts/Simulation/Combat/ClassSwapUtil.cs @@ -18,13 +18,16 @@ namespace ProjectM.Simulation /// Re-seed the class band + re-sync the permanent-meta band for on /// . Returns the normalized class + its Fire ability id (the caller sets AbilityRef). /// false (no catalog/record) skips the meta replay (the strip still runs). + /// Re-seed the class band + re-sync the permanent-meta band for on + /// . Returns the normalized class (the caller writes FrameId/PlayerClass + re-seeds + /// the socket loadout). false (no catalog/record) skips the meta replay (the + /// strip still runs). public static void Apply(byte rawClass, DynamicBuffer mods, bool haveMeta, in MetaUpgradeCatalog metaCat, DynamicBuffer metaRecord, - out byte newClass, out byte newAbilityId) + out byte newClass) { newClass = ClassTraits.Normalize(rawClass); ClassTraits.Reapply(newClass, mods); - newAbilityId = ClassTraits.AbilityFor(newClass); // Strip the OLD class's meta rows (Reapply only touched the class-seed band), then replay the NEW class's // persisted tiers (the GoInGame skip/clamp rules) so the permanent channel stays correct across the swap. diff --git a/Assets/_Project/Scripts/Simulation/Combat/ClassTraits.cs b/Assets/_Project/Scripts/Simulation/Combat/ClassTraits.cs index 302cc583a..0682be8c9 100644 --- a/Assets/_Project/Scripts/Simulation/Combat/ClassTraits.cs +++ b/Assets/_Project/Scripts/Simulation/Combat/ClassTraits.cs @@ -36,14 +36,12 @@ namespace ProjectM.Simulation public static byte Normalize(byte classId) => classId == RangerClass ? RangerClass : WarriorClass; /// The Fire-slot ability id for a class (Warrior = cone, Ranger = the default projectile). - public static byte AbilityFor(byte classId) - => classId == RangerClass ? (byte)AbilityId.Primary : (byte)AbilityId.WarriorCone; + /// The class a Fire-slot ability id implies — the exact inverse of (Ranger /// iff Primary). Lets the CLIENT derive the local class from the replicated (tracks /// the dev class-switch, unlike the menu's ClassSelection static; PlayerClass itself is server-only). - public static byte ClassForAbility(byte abilityId) - => abilityId == (byte)AbilityId.Primary ? RangerClass : WarriorClass; + /// Default 4-socket Spark loadout per frame (Harpooner = the Ranger slot, line-and-iron; Bathynaut = /// the Warrior slot, anchor-and-crash). Tunable; drives the gym's per-frame default sockets (Build Spec step 5). diff --git a/Assets/_Project/Scripts/Simulation/Combat/EffectiveAbilityStats.cs b/Assets/_Project/Scripts/Simulation/Combat/EffectiveAbilityStats.cs deleted file mode 100644 index 0f41166be..000000000 --- a/Assets/_Project/Scripts/Simulation/Combat/EffectiveAbilityStats.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Unity.Entities; - -namespace ProjectM.Simulation -{ - /// - /// Per-entity effective ability stats: the authored base (from the AbilityDatabase blob keyed by - /// AbilityRef) folded with the entity's StatModifier buffer by StatRecomputeSystem each predicted - /// tick. Derived/local, NOT replicated - both worlds recompute it deterministically from the - /// replicated modifier buffer, so it matches under prediction without being in the snapshot. - /// - public struct EffectiveAbilityStats : IComponentData - { - public float Damage; - public float ProjectileSpeed; - public float Range; - public float AutoTargetRange; - public float AutoTargetConeRadians; - public int CooldownTicks; - } -} diff --git a/Assets/_Project/Scripts/Simulation/Combat/EffectiveAbilityStats.cs.meta b/Assets/_Project/Scripts/Simulation/Combat/EffectiveAbilityStats.cs.meta deleted file mode 100644 index e63a65cb2..000000000 --- a/Assets/_Project/Scripts/Simulation/Combat/EffectiveAbilityStats.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a8bb3a5c343e74e7fb249e96c0c55fdc \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Combat/StatRecomputeSystem.cs b/Assets/_Project/Scripts/Simulation/Combat/StatRecomputeSystem.cs index b70b349ca..20992c604 100644 --- a/Assets/_Project/Scripts/Simulation/Combat/StatRecomputeSystem.cs +++ b/Assets/_Project/Scripts/Simulation/Combat/StatRecomputeSystem.cs @@ -7,8 +7,9 @@ namespace ProjectM.Simulation { /// /// Folds each modifiable entity's authored base stats (from the AbilityDatabase blob, keyed by - /// AbilityRef / CharacterStatsRef) with its replicated StatModifier buffer into the - /// EffectiveAbilityStats / EffectiveCharacterStats components - every predicted tick, on both worlds. + /// CharacterStatsRef / the AbilitySocket loadout) with its replicated StatModifier buffer into the + /// EffectiveCharacterStats / EffectiveSocketStats components - every predicted tick, on both worlds. + /// (The legacy single AbilityRef -> EffectiveAbilityStats fold is deleted — LANTERN purge.) /// /// Runs at the head of the predicted group (UpdateBefore PlayerAimSystem; /// AbilityFireSystem runs after PlayerAimSystem, so it sees fresh values too). Recompute is @@ -34,24 +35,11 @@ namespace ProjectM.Simulation var database = SystemAPI.GetSingleton(); ref var db = ref database.Value.Value; - foreach (var (abilityRef, charRef, mods, effAbility, effChar) in - SystemAPI.Query, RefRO, DynamicBuffer, - RefRW, RefRW>() + foreach (var (charRef, mods, effChar) in + SystemAPI.Query, DynamicBuffer, + RefRW>() .WithAll()) { - if (db.TryGetAbility(abilityRef.ValueRO.Id, out var a)) - { - effAbility.ValueRW = new EffectiveAbilityStats - { - Damage = StatMath.Apply(a.Damage, StatTarget.Damage, mods), - ProjectileSpeed = StatMath.Apply(a.ProjectileSpeed, StatTarget.ProjectileSpeed, mods), - Range = StatMath.Apply(a.Range, StatTarget.Range, mods), - AutoTargetRange = StatMath.Apply(a.AutoTargetRange, StatTarget.AutoTargetRange, mods), - AutoTargetConeRadians = StatMath.Apply(a.AutoTargetConeRadians, StatTarget.AutoTargetConeRadians, mods), - CooldownTicks = (int)math.round(StatMath.Apply(a.CooldownTicks, StatTarget.CooldownTicks, mods)), - }; - } - if (db.TryGetCharacter(charRef.ValueRO.Id, out var c)) { effChar.ValueRW = new EffectiveCharacterStats @@ -65,8 +53,7 @@ namespace ProjectM.Simulation // LANTERN Phase 1 (Step 1b): per-socket fold - each socket's Spark base folded with the SHARED // StatModifier band into its EffectiveSocketStats row (uniform band; per-Spark warping is Phase 4). - // Separate query so this system stays under the 7-arg cap; the legacy single fold above stays - // until AbilityFireSystem + the feel layer migrate to the buffer (steps 2/2.5). + // Separate query so this system stays under the 7-arg cap. foreach (var (sockets, socketMods, effSockets) in SystemAPI.Query, DynamicBuffer, DynamicBuffer>() .WithAll()) diff --git a/Assets/_Project/Scripts/Simulation/Items/DefaultAbility.cs b/Assets/_Project/Scripts/Simulation/Items/DefaultAbility.cs deleted file mode 100644 index ba9a9a218..000000000 --- a/Assets/_Project/Scripts/Simulation/Items/DefaultAbility.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Unity.Entities; - -namespace ProjectM.Simulation -{ - /// - /// The player's "unarmed" / base ability id, baked from PlayerAuthoring.PrimaryAbility. Restored into - /// .Id by EquipSystem when a weapon is unequipped. NOT replicated (it never changes, - /// so a [GhostField] would waste snapshot bytes and there is no client consumer). AbilityRef itself cannot - /// serve double duty because EquipSystem overwrites AbilityRef.Id when a weapon is equipped — this preserves - /// the immutable default to fall back to. Server-read only. - /// - public struct DefaultAbility : IComponentData - { - /// The (as a byte) the player fires with no weapon equipped. - public byte Id; - } -} diff --git a/Assets/_Project/Scripts/Simulation/Items/DefaultAbility.cs.meta b/Assets/_Project/Scripts/Simulation/Items/DefaultAbility.cs.meta deleted file mode 100644 index 0ccdf44ed..000000000 --- a/Assets/_Project/Scripts/Simulation/Items/DefaultAbility.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 4c6831e7f8bb98d448917f88dcbe12db \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs b/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs index 34bfcde5f..501d41b11 100644 --- a/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs +++ b/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs @@ -50,9 +50,6 @@ namespace ProjectM.Simulation /// Equip slot (see ); 255 = not equippable. public byte EquipSlot; - /// AbilityId granted when equipped in the Weapon slot (0 = none); the equip handler writes it into AbilityRef.Id. - public byte GrantedAbilityId; - /// Up to INLINE stat-mod grants applied while equipped (Target 255 = unused). Inline, not a nested BlobArray. public ItemModSpec Mod0, Mod1, Mod2, Mod3; diff --git a/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs b/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs index 03f1c5ed1..ed87e7f88 100644 --- a/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs +++ b/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs @@ -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)); diff --git a/Assets/_Project/Tests/EditMode/EquipSystemTests.cs b/Assets/_Project/Tests/EditMode/EquipSystemTests.cs index e62db0bff..9acb1ec4d 100644 --- a/Assets/_Project/Tests/EditMode/EquipSystemTests.cs +++ b/Assets/_Project/Tests/EditMode/EquipSystemTests.cs @@ -10,11 +10,12 @@ namespace ProjectM.Tests { /// /// Plain-Entities EditMode tests for the server-only . 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. /// 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(); 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(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(e); - em.AddComponentData(e, new AbilityRef { Id = (byte)AbilityId.Primary }); - em.AddComponentData(e, new DefaultAbility { Id = (byte)AbilityId.Primary }); var bag = em.AddBuffer(e); foreach (var it in bagItems) bag.Add(new InventorySlot { ItemId = it.id, Count = it.count }); var slots = em.AddBuffer(e); @@ -102,7 +101,6 @@ namespace ProjectM.Tests em.AddComponentData(e, new ReceiveRpcCommandRequest { SourceConnection = conn }); } - static byte Ability(EntityManager em, Entity p) => em.GetComponentData(p).Id; static ushort Slot(EntityManager em, Entity p, byte slot) => em.GetBuffer(p)[slot].ItemId; static int Bag(EntityManager em, Entity p, ushort id) => InventoryMath.CountOf(em.GetBuffer(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."); } } diff --git a/Assets/_Project/Tests/EditMode/MetaSeedingTests.cs b/Assets/_Project/Tests/EditMode/MetaSeedingTests.cs index 224fdb029..a9926cede 100644 --- a/Assets/_Project/Tests/EditMode/MetaSeedingTests.cs +++ b/Assets/_Project/Tests/EditMode/MetaSeedingTests.cs @@ -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(e); + em.AddBuffer(e); // the spawn path SetBuffers the frame loadout unconditionally (LANTERN purge) em.AddComponent(e); return e; } diff --git a/Assets/_Project/Tests/EditMode/StatRecomputeSystemTests.cs b/Assets/_Project/Tests/EditMode/StatRecomputeSystemTests.cs index f04b50bc9..433832dd3 100644 --- a/Assets/_Project/Tests/EditMode/StatRecomputeSystemTests.cs +++ b/Assets/_Project/Tests/EditMode/StatRecomputeSystemTests.cs @@ -7,9 +7,10 @@ namespace ProjectM.Tests { /// /// Plain-Entities test for : 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. /// 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().Update(); - var ea = world.EntityManager.GetComponentData(player); var ec = world.EntityManager.GetComponentData(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().Update(); - var ea = world.EntityManager.GetComponentData(player); var ec = world.EntityManager.GetComponentData(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(); group.Update(); - var first = world.EntityManager.GetComponentData(player).Damage; + var first = world.EntityManager.GetComponentData(player).MaxHealth; for (int i = 0; i < 5; i++) group.Update(); - var last = world.EntityManager.GetComponentData(player).Damage; - Assert.AreEqual(30f, first, 1e-3f); + var last = world.EntityManager.GetComponentData(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(player); var effBuf = em.GetBuffer(player);