Dev tool: switch player class (Warrior/Ranger) at runtime for testing
Editor-only class swap via the existing scalar dev-RPC family (new DebugOp.SetClass): F1/F2 keybind (ClassSwitchHotkeySystem), DebugOverlay '- Class -' buttons, and DebugCommandSendSystem.SetWarrior/SetRanger/SetClass statics. Server (DebugCommandReceiveSystem) swaps class in place on the spawned player: strips+re-seeds the ClassTraits StatModifier seeds, swaps the AbilityRef Fire slot, resets the ability cooldown, and heals a LIVING player to the new max (dead players skip the heal so respawn isn't raced). Server-authoritative + prediction-correct (same buffer-mutation path as GrantUpgrade); wire type unchanged so the RpcCollection hash is unaffected. ClassTraits gains a shared Seeds core (spawn + swap can't drift), ClassSeedCount, IsClassSeed, a DynamicBuffer AppendSeeds overload, and Reapply. +3 EditMode tests (exact-count round-trip, value-equality fold, boundary/foreign-mod preservation); 351/351 green; Warrior<->Ranger round-trip Play-validated (server+client agree). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -172,6 +172,41 @@ namespace ProjectM.Server
|
||||
SystemAPI.SetSingleton(tuningCfg);
|
||||
}
|
||||
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)
|
||||
&& SystemAPI.HasBuffer<StatModifier>(sender))
|
||||
{
|
||||
byte newClass = ClassTraits.Normalize((byte)cmd.ArgA);
|
||||
var classMods = SystemAPI.GetBuffer<StatModifier>(sender);
|
||||
ClassTraits.Reapply(newClass, classMods);
|
||||
SystemAPI.SetComponent(sender, new AbilityRef { Id = ClassTraits.AbilityFor(newClass) });
|
||||
|
||||
// Let the swapped Fire ability fire immediately (both abilities share one cooldown gate).
|
||||
if (SystemAPI.HasComponent<AbilityCooldown>(sender))
|
||||
SystemAPI.SetComponent(sender, new AbilityCooldown { NextFireTick = 0 }); // 0 = ready
|
||||
|
||||
// Heal a living player to the new class's full max (fold blob base + the just-reseeded
|
||||
// buffer, like StatRecomputeSystem; Effective* still lags a tick here). Doubles as the
|
||||
// down-clamp when the new max is lower (nothing else clamps Current off a damage event).
|
||||
if (SystemAPI.HasComponent<Health>(sender) && SystemAPI.HasComponent<CharacterStatsRef>(sender)
|
||||
&& SystemAPI.TryGetSingleton<AbilityDatabase>(out var abilityDb))
|
||||
{
|
||||
var hp = SystemAPI.GetComponent<Health>(sender);
|
||||
byte charId = SystemAPI.GetComponent<CharacterStatsRef>(sender).Id;
|
||||
if (hp.Current > 0f && abilityDb.Value.Value.TryGetCharacter(charId, out var baseChar))
|
||||
{
|
||||
hp.Current = StatMath.Apply(baseChar.MaxHealth, StatTarget.MaxHealth, classMods);
|
||||
SystemAPI.SetComponent(sender, hp);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(reqEntity);
|
||||
|
||||
Reference in New Issue
Block a user