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:
2026-07-15 15:47:12 -07:00
parent b34945c2d2
commit 4a8220ad3e
27 changed files with 137 additions and 275 deletions
@@ -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<AbilityRef>(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<AbilitySocket>(sender)
&& SystemAPI.HasBuffer<StatModifier>(sender))
{
var classMods = SystemAPI.GetBuffer<StatModifier>(sender);
@@ -156,15 +156,23 @@ namespace ProjectM.Server
bool haveMeta2 = SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out var metaCat2)
&& SystemAPI.TryGetSingletonEntity<ResourceLedger>(out dir2) && SystemAPI.HasBuffer<MetaTierState>(dir2);
var metaRec2 = haveMeta2 ? SystemAPI.GetBuffer<MetaTierState>(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<FrameId>(sender))
SystemAPI.SetComponent(sender, new FrameId { Value = swNewClass });
if (SystemAPI.HasComponent<PlayerClass>(sender))
SystemAPI.SetComponent(sender, new PlayerClass { ClassId = swNewClass });
if (SystemAPI.HasComponent<AbilityCooldown>(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<AbilitySocket>(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<SocketCooldown>(sender))
SystemAPI.SetComponent(sender, default(SocketCooldown));
if (SystemAPI.HasComponent<Health>(sender) && SystemAPI.HasComponent<CharacterStatsRef>(sender)
&& SystemAPI.TryGetSingleton<AbilityDatabase>(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<LocalTransform>(sender)