Files
kronic a74b761363 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>
2026-06-18 21:23:33 -07:00

71 lines
3.7 KiB
C#

#if UNITY_EDITOR
using System.Collections.Generic;
using ProjectM.Simulation;
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
namespace ProjectM.Client
{
/// <summary>
/// EDITOR-ONLY client sender for dev-tool <see cref="DebugCommandRequest"/> RPCs. Mirrors
/// <c>StorageOpSendSystem</c>: static convenience methods enqueue into a queue that this client
/// <see cref="SystemBase"/> drains into request entities each tick (so it works from the DebugOverlay's IMGUI
/// AND headless from execute_code). The statics are reset on play-enter so a fast-enter-playmode reload can't
/// replay a stale queue. The wire type is unconditional; this system is #if UNITY_EDITOR (stripped from builds).
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial class DebugCommandSendSystem : SystemBase
{
struct Pending { public byte Op; public int ArgA; public int ArgB; }
static readonly List<Pending> s_Pending = new List<Pending>();
/// <summary>Queue a raw dev command for the next client tick.</summary>
public static void Send(byte op, int argA = 0, int argB = 0)
=> s_Pending.Add(new Pending { Op = op, ArgA = argA, ArgB = argB });
// Convenience wrappers (overlay buttons + execute_code).
public static void SpawnWave(int size) => Send(DebugOp.SpawnWave, size);
public static void EndSiege() => Send(DebugOp.EndSiege);
public static void ClearEnemies() => Send(DebugOp.ClearEnemies);
public static void SetCalm() => Send(DebugOp.SetCalm);
public static void GrantResource(byte itemId, int count) => Send(DebugOp.GrantResource, itemId, count);
public static void GrantUpgrade() => Send(DebugOp.GrantUpgrade);
public static void Teleport(byte region) => Send(DebugOp.Teleport, region);
public static void ToggleGod() => Send(DebugOp.ToggleGod);
public static void Heal() => Send(DebugOp.Heal);
public static void Kill() => Send(DebugOp.KillPlayer);
public static void AdvanceGoal(int by) => Send(DebugOp.AdvanceGoal, by);
public static void SetHeat(int heat) => Send(DebugOp.SetHeat, heat);
/// <summary>Set the <see cref="ProjectM.Simulation.TuningKnob"/> knob to value (server-applied, x1000 fixed-point; MC-0).</summary>
public static void SetTuning(byte knob, float value) => Send(DebugOp.SetTuning, knob, Mathf.RoundToInt(value * 1000f));
/// <summary>Swap the sender's class to <paramref name="classId"/> (a <see cref="ProjectM.Simulation.CharacterId"/> byte); server-authoritative (class-switch dev tool).</summary>
public static void SetClass(byte classId) => Send(DebugOp.SetClass, classId);
public static void SetWarrior() => SetClass(ClassTraits.WarriorClass);
public static void SetRanger() => SetClass(ClassTraits.RangerClass);
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetOnEnterPlayMode() => s_Pending.Clear();
protected override void OnUpdate()
{
if (s_Pending.Count == 0)
return;
if (!SystemAPI.TryGetSingletonEntity<NetworkId>(out var connection))
return; // not connected yet — hold the queue
var em = EntityManager;
for (int i = 0; i < s_Pending.Count; i++)
{
var p = s_Pending[i];
var req = em.CreateEntity();
em.AddComponentData(req, new DebugCommandRequest { Op = p.Op, ArgA = p.ArgA, ArgB = p.ArgB });
em.AddComponentData(req, new SendRpcCommandRequest { TargetConnection = connection });
}
s_Pending.Clear();
}
}
}
#endif