From 04d7b42e60f093ae6830562f26b6edefab395d49 Mon Sep 17 00:00:00 2001 From: Ian Woods Date: Tue, 21 Jul 2026 20:37:57 -0700 Subject: [PATCH] basic developer console --- Assets/_Project/Prefabs/Player/Player.prefab | 14 +++ .../_Project/Scripts/Dev/DebugCommandRelay.cs | 78 +++++++++++++++ .../Scripts/Dev/DebugCommandRelay.cs.meta | 2 + Assets/_Project/Scripts/Dev/DebugConsole.cs | 98 +++++++++++++++++-- 4 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 Assets/_Project/Scripts/Dev/DebugCommandRelay.cs create mode 100644 Assets/_Project/Scripts/Dev/DebugCommandRelay.cs.meta diff --git a/Assets/_Project/Prefabs/Player/Player.prefab b/Assets/_Project/Prefabs/Player/Player.prefab index 08614f3..6e06dfd 100644 --- a/Assets/_Project/Prefabs/Player/Player.prefab +++ b/Assets/_Project/Prefabs/Player/Player.prefab @@ -18,6 +18,7 @@ GameObject: - component: {fileID: 2806524246861401801} - component: {fileID: 5683786710272852339} - component: {fileID: 6856236869205671849} + - component: {fileID: -4438158474568445526} m_Layer: 0 m_Name: Player m_TagString: Untagged @@ -173,3 +174,16 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerSpellLoadout ShowTopMostFoldoutHeaderGroup: 1 +--- !u!114 &-4438158474568445526 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3493329038866903420} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 65825b3189ae873948659b37b219f5e6, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::TD.Dev.DebugCommandRelay + ShowTopMostFoldoutHeaderGroup: 1 diff --git a/Assets/_Project/Scripts/Dev/DebugCommandRelay.cs b/Assets/_Project/Scripts/Dev/DebugCommandRelay.cs new file mode 100644 index 0000000..3af3076 --- /dev/null +++ b/Assets/_Project/Scripts/Dev/DebugCommandRelay.cs @@ -0,0 +1,78 @@ +// Assets/_Project/Scripts/Dev/DebugCommandRelay.cs +using System.Collections.Generic; +using Unity.Netcode; +using UnityEngine; +using TD.Gameplay.Draft; + +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}."); + } + } +} diff --git a/Assets/_Project/Scripts/Dev/DebugCommandRelay.cs.meta b/Assets/_Project/Scripts/Dev/DebugCommandRelay.cs.meta new file mode 100644 index 0000000..d7a2598 --- /dev/null +++ b/Assets/_Project/Scripts/Dev/DebugCommandRelay.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 65825b3189ae873948659b37b219f5e6 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Dev/DebugConsole.cs b/Assets/_Project/Scripts/Dev/DebugConsole.cs index d88ca20..869be74 100644 --- a/Assets/_Project/Scripts/Dev/DebugConsole.cs +++ b/Assets/_Project/Scripts/Dev/DebugConsole.cs @@ -1,9 +1,13 @@ // Assets/_Project/Scripts/Dev/DebugConsole.cs +using System; using System.Collections; +using System.Collections.Generic; +using System.Text; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UIElements; using TD.UI; +using TD.Gameplay.Draft; namespace TD.Dev { @@ -17,9 +21,9 @@ namespace TD.Dev /// a build entirely by not including that GameObject, the same way /// is kept separate from shipping systems. /// - /// This first pass only wires up open/close/focus/text-capture. Submit - /// currently just logs the typed text — command parsing/dispatch (e.g. - /// unlocking spells) comes next. + /// Supports one command so far: "get <type> <name>" grants a + /// to the local player via + /// . /// [RequireComponent(typeof(UIDocument))] public class DebugConsole : MonoBehaviour @@ -204,15 +208,93 @@ namespace TD.Dev HUDController.SuppressChatOpenUntilFrame = Time.frameCount + 1; } - // Placeholder — command parsing/dispatch (unlocking spells, granting gold, - // etc.) comes next. For now this just proves the text made it through. private void SubmitCommand() { string text = commandInput?.value ?? string.Empty; - if (!string.IsNullOrWhiteSpace(text)) - Debug.Log($"[DebugConsole] Command entered: \"{text}\""); - CloseConsole(); + + if (string.IsNullOrWhiteSpace(text)) return; + ExecuteCommand(text.Trim()); + } + + // Maps the type token in "get " to the DraftOption subclass it + // should match against. Add an entry here whenever a new DraftOption subclass + // should be gettable from the console. + private static readonly Dictionary DraftTypeAliases = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "spell", typeof(BuilderSpellDraftOption) }, + { "tower", typeof(NewTowerDraftOption) }, + { "buff", typeof(BuilderEffectDraftOption) }, + { "effect", typeof(BuilderEffectDraftOption) }, + }; + + private void ExecuteCommand(string text) + { + string[] tokens = text.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length < 3 || !string.Equals(tokens[0], "get", StringComparison.OrdinalIgnoreCase)) + { + Debug.Log($"[DebugConsole] Unrecognized command: \"{text}\". Try: get "); + return; + } + + string nameToken = string.Join(" ", tokens, 2, tokens.Length - 2); + ExecuteGet(tokens[1], nameToken); + } + + // "get ": grants any DraftOption of the given type whose DisplayName + // matches , via the local player's DebugCommandRelay. Reuses DraftOption's + // existing type-agnostic ServerApply dispatch instead of hardcoding a grant path + // per option type. + private void ExecuteGet(string typeToken, string nameToken) + { + if (!DraftTypeAliases.TryGetValue(typeToken, out var optionType)) + { + Debug.LogWarning($"[DebugConsole] Unknown draft option type \"{typeToken}\". " + + $"Valid types: {string.Join(", ", DraftTypeAliases.Keys)}"); + return; + } + + var pool = DraftPool.Instance; + if (pool == null) + { + Debug.LogWarning("[DebugConsole] No DraftPool in this scene."); + return; + } + + string normalizedTarget = Normalize(nameToken); + for (int i = 0; i < pool.Count; i++) + { + var option = pool.Get(i); + if (option == null || !optionType.IsInstanceOfType(option)) continue; + if (Normalize(option.DisplayName) != normalizedTarget) continue; + + var relay = DebugCommandRelay.Local; + if (relay == null) + { + Debug.LogWarning("[DebugConsole] No local DebugCommandRelay found — " + + "is it on the Player prefab?"); + return; + } + + relay.DebugGrantDraftOptionRpc(i); + return; + } + + Debug.LogWarning($"[DebugConsole] No {typeToken} option named \"{nameToken}\" found."); + } + + // DisplayName is player-facing draft card text (its spacing/casing is chosen for + // presentation, not for typing), so command matching strips everything but letters + // and digits before comparing — "Slow Area", "slow_area", and "SlowArea" all match + // the same option. + private static string Normalize(string s) + { + if (string.IsNullOrEmpty(s)) return string.Empty; + var sb = new StringBuilder(s.Length); + foreach (char c in s) + if (char.IsLetterOrDigit(c)) sb.Append(char.ToLowerInvariant(c)); + return sb.ToString(); } private void OnDestroy()