// Assets/_Project/Scripts/Dev/DebugCommandRelay.cs using System.Collections.Generic; using Unity.Netcode; using UnityEngine; using TD.Gameplay.Draft; using TD.Gameplay.EnemyUpgrades; using TD.Gameplay.Waves; namespace TD.Dev { /// /// Debug-only network relay: exposes one RPC that force-applies a /// by its index, bypassing PlayerDraft's "must currently be /// offered" check. /// /// /// Lives entirely separate from PlayerDraft/DraftOption/DraftPool — none of those files /// know this component exists. Removing this component (and TD.Dev.DebugConsole, which /// calls it) leaves the shipping draft system completely untouched. /// /// Added to the Player prefab alongside PlayerDraft/PlayerGoldManager/etc. so it spawns /// with a per-player OwnerClientId. Static per-client registry mirrors PlayerDraft's /// GetForClient/Local pattern. /// public class DebugCommandRelay : NetworkBehaviour { private static readonly Dictionary s_byClientId = new Dictionary(); public static DebugCommandRelay GetForClient(ulong clientId) { s_byClientId.TryGetValue(clientId, out var relay); return relay; } public static DebugCommandRelay Local { get { var nm = NetworkManager.Singleton; if (nm == null || !nm.IsClient) return null; return GetForClient(nm.LocalClientId); } } public override void OnNetworkSpawn() { s_byClientId[OwnerClientId] = this; } public override void OnNetworkDespawn() { if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this) s_byClientId.Remove(OwnerClientId); } /// /// Owning client: force-apply the DraftOption at (its /// DraftPool index) immediately, skipping the normal "must be currently offered" gate. /// Debug/testing only — any connected client can grant themselves anything via this /// RPC, same tradeoff DevWaveControls already accepts for its hotkeys. /// [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] public void DebugGrantDraftOptionRpc(int optionId) { var option = DraftPool.Instance != null ? DraftPool.Instance.Get(optionId) : null; if (option == null) { Debug.LogWarning($"[DebugCommandRelay] Grant requested for unknown option id {optionId}."); return; } if (option.ServerApply(OwnerClientId)) Debug.Log($"[DebugCommandRelay] Granted '{option.DisplayName}' to client {OwnerClientId}."); else Debug.LogWarning($"[DebugCommandRelay] '{option.DisplayName}' could not be applied " + $"to client {OwnerClientId}."); } /// /// Force-apply the at (its /// index) to the run's current wave slot — the same /// recording WaveVote.ServerResolve does for a vote winner, skipping the vote /// entirely. Not player-scoped: the effect lands on the run's current wave slot rather /// than the caller, so any connected client's relay can trigger it. /// /// /// Takes effect the next time this wave slot's enemies spawn, not retroactively on /// enemies already alive — see 's remarks on why /// applying is "record on the slot," not a direct mutation. /// [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] public void DebugGrantEnemyUpgradeRpc(int optionId) { var pool = EnemyUpgradePool.Instance; var option = pool != null ? pool.Get(optionId) : null; if (option == null) { Debug.LogWarning($"[DebugCommandRelay] Grant requested for unknown enemy upgrade id {optionId}."); return; } var run = RunState.Instance; if (run == null) { Debug.LogWarning("[DebugCommandRelay] No RunState in this scene."); return; } int slot = run.CurrentUpgradeSlot; run.ServerAddUpgrade(slot, optionId); option.ServerOnApplied(run, slot); Debug.Log($"[DebugCommandRelay] Granted '{option.DisplayName}' to wave slot {slot}."); } } }