From 4375350901ad078c6a997641e4c1415622acb5dc Mon Sep 17 00:00:00 2001 From: Ian Woods Date: Mon, 20 Jul 2026 23:48:20 -0700 Subject: [PATCH] added text entry system for debug console --- Assets/_Project/Scenes/Levels/9Player.unity | 70 ++++++ Assets/_Project/Scripts/Dev/DebugConsole.cs | 223 ++++++++++++++++++ .../_Project/Scripts/Dev/DebugConsole.cs.meta | 2 + Assets/_Project/Scripts/UI/HUDController.cs | 17 +- 4 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 Assets/_Project/Scripts/Dev/DebugConsole.cs create mode 100644 Assets/_Project/Scripts/Dev/DebugConsole.cs.meta diff --git a/Assets/_Project/Scenes/Levels/9Player.unity b/Assets/_Project/Scenes/Levels/9Player.unity index 0fcac59..8c21309 100644 --- a/Assets/_Project/Scenes/Levels/9Player.unity +++ b/Assets/_Project/Scenes/Levels/9Player.unity @@ -13757,6 +13757,75 @@ Mesh: - serializedVersion: 1 m_IndexStart: 0 m_IndexCount: 0 +--- !u!1 &954434158 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 954434161} + - component: {fileID: 954434160} + - component: {fileID: 954434159} + m_Layer: 0 + m_Name: DebugConsole + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &954434159 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 954434158} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7c6a808e8291552d0b64e36a210879c4, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::TD.Dev.DebugConsole + toggleKey: 4 +--- !u!114 &954434160 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 954434158} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument + m_PanelSettings: {fileID: 11400000, guid: 6aa0af71585acea4db4995c3931dc946, type: 2} + m_ParentUI: {fileID: 0} + sourceAsset: {fileID: 0} + m_SortingOrder: 1 + m_Position: 0 + m_WorldSpaceSizeMode: 1 + m_WorldSpaceWidth: 1920 + m_WorldSpaceHeight: 1080 + m_PivotReferenceSize: 0 + m_Pivot: 0 + m_WorldSpaceCollider: {fileID: 0} +--- !u!4 &954434161 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 954434158} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -52.51635, y: 0, z: -27.6311} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &955672358 GameObject: m_ObjectHideFlags: 0 @@ -29118,3 +29187,4 @@ SceneRoots: - {fileID: 914832726} - {fileID: 993870445} - {fileID: 759470735} + - {fileID: 954434161} diff --git a/Assets/_Project/Scripts/Dev/DebugConsole.cs b/Assets/_Project/Scripts/Dev/DebugConsole.cs new file mode 100644 index 0000000..d88ca20 --- /dev/null +++ b/Assets/_Project/Scripts/Dev/DebugConsole.cs @@ -0,0 +1,223 @@ +// Assets/_Project/Scripts/Dev/DebugConsole.cs +using System.Collections; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.UIElements; +using TD.UI; + +namespace TD.Dev +{ + /// + /// Standalone debug console overlay: press the toggle key to open a text + /// input, type a command, Enter to submit, Escape to cancel. + /// + /// + /// Deliberately decoupled from — own + /// GameObject, own UIDocument/PanelSettings — so it can be dropped out of + /// 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. + /// + [RequireComponent(typeof(UIDocument))] + public class DebugConsole : MonoBehaviour + { + public static DebugConsole Instance { get; private set; } + + [Tooltip("Keyboard shortcut that opens and closes the console. Uses the new Input " + + "System (UnityEngine.InputSystem.Key). Backquote is the physical ` / ~ key.")] + [SerializeField] private Key toggleKey = Key.Backquote; + + private VisualElement consoleContainer; + private TextField commandInput; + private bool consoleOpen; + + // Frame on which the console was opened or closed. The toggle key and Enter + // are ignored on that frame and the next so the keypress that opened/closed + // the console doesn't also get typed into the field or immediately submit it. + // Mirrors HUDController's chatToggleSuppressFrame. + private int toggleSuppressFrame = -1; + + private void Awake() + { + if (Instance != null && Instance != this) + { + Debug.LogError("[DebugConsole] Duplicate instance detected. Destroying the new one."); + Destroy(gameObject); + return; + } + Instance = this; + } + + private void Start() + { + var doc = GetComponent(); + if (doc == null) + { + Debug.LogError("[DebugConsole] No UIDocument component found."); + return; + } + + var root = doc.rootVisualElement; + if (root == null) + { + Debug.LogError("[DebugConsole] rootVisualElement is null. " + + "Check that Panel Settings is assigned."); + return; + } + + BuildConsoleUi(root); + } + + private void BuildConsoleUi(VisualElement uiRoot) + { + // Top-anchored bar, closed by default. Distinct position from the HUD's + // bottom-left chat panel so the two never overlap. + consoleContainer = new VisualElement(); + consoleContainer.pickingMode = PickingMode.Ignore; + consoleContainer.style.position = Position.Absolute; + consoleContainer.style.left = 0; + consoleContainer.style.right = 0; + consoleContainer.style.top = 0; + consoleContainer.style.paddingTop = 8; + consoleContainer.style.paddingBottom = 8; + consoleContainer.style.paddingLeft = 12; + consoleContainer.style.paddingRight = 12; + consoleContainer.style.backgroundColor = new Color(0f, 0f, 0f, 0.75f); + consoleContainer.style.display = DisplayStyle.None; + + commandInput = new TextField(); + commandInput.style.minHeight = 28; + commandInput.style.width = Length.Percent(100); + commandInput.maxLength = 200; + commandInput.isDelayed = false; + StyleConsoleInput(commandInput); + + // Focus tracking — contributes to the same flag HUDController's chat + // input sets, so camera/builder/spell input all stay gated correctly + // regardless of which text field currently has focus. + commandInput.RegisterCallback(_ => HUDController.IsTextInputActive = true); + commandInput.RegisterCallback(_ => HUDController.IsTextInputActive = false); + + consoleContainer.Add(commandInput); + uiRoot.Add(consoleContainer); + } + + // Mirrors HUDController.StyleChatInputDark — dark background, white text, + // a thin border, and inner padding so ascenders/descenders don't clip. + // Green border (vs. chat's neutral gray) as a quick visual "this is the + // debug console, not chat" cue. + private static void StyleConsoleInput(TextField field) + { + var darkBg = new Color(0.05f, 0.05f, 0.05f, 0.95f); + var borderClr = new Color(0.35f, 0.75f, 0.35f); + + field.style.backgroundColor = darkBg; + field.style.color = Color.white; + field.style.borderTopWidth = field.style.borderBottomWidth = + field.style.borderLeftWidth = field.style.borderRightWidth = 1; + field.style.borderTopColor = field.style.borderBottomColor = + field.style.borderLeftColor = field.style.borderRightColor = borderClr; + + var inner = field.Q("unity-text-input"); + if (inner != null) + { + inner.style.backgroundColor = darkBg; + inner.style.color = Color.white; + inner.style.paddingTop = 4; + inner.style.paddingBottom = 4; + inner.style.paddingLeft = 6; + inner.style.paddingRight = 6; + } + } + + private void Update() + { + var kb = Keyboard.current; + if (kb == null) return; // no keyboard connected (e.g. headless server) + + if (Time.frameCount <= toggleSuppressFrame) return; + + if (toggleKey != Key.None && kb[toggleKey].wasPressedThisFrame) + { + if (consoleOpen) CloseConsole(); + else OpenConsole(); + return; + } + + if (!consoleOpen) return; + + bool enterDown = kb.enterKey.wasPressedThisFrame || kb.numpadEnterKey.wasPressedThisFrame; + bool escDown = kb.escapeKey.wasPressedThisFrame; + + if (enterDown) SubmitCommand(); + else if (escDown) CloseConsole(); + } + + private void OpenConsole() + { + if (commandInput == null) return; + consoleContainer.style.display = DisplayStyle.Flex; + consoleContainer.pickingMode = PickingMode.Position; + commandInput.SetValueWithoutNotify(string.Empty); + + // Suppress the toggle key and Enter for this frame and the next so the + // keypress that opened the console doesn't also get typed into the field. + // Focus is deferred a frame for the same reason — UI Toolkit would + // otherwise route the open keypress to the freshly-focused TextField. + toggleSuppressFrame = Time.frameCount + 1; + consoleOpen = true; + StartCoroutine(FocusNextFrame()); + } + + private IEnumerator FocusNextFrame() + { + yield return null; + if (consoleOpen && commandInput != null) + { + commandInput.Focus(); + commandInput.SelectAll(); + } + } + + private void CloseConsole() + { + if (commandInput != null) + { + commandInput.SetValueWithoutNotify(string.Empty); + commandInput.Blur(); + } + if (consoleContainer != null) + { + consoleContainer.style.display = DisplayStyle.None; + consoleContainer.pickingMode = PickingMode.Ignore; + } + + consoleOpen = false; + toggleSuppressFrame = Time.frameCount + 1; + + // The Enter that submitted/cancelled this console also gets polled by + // HUDController's chat this same frame — suppress its "Enter opens chat" + // branch so closing the debug console doesn't pop chat open too. + 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(); + } + + private void OnDestroy() + { + if (Instance == this) Instance = null; + } + } +} diff --git a/Assets/_Project/Scripts/Dev/DebugConsole.cs.meta b/Assets/_Project/Scripts/Dev/DebugConsole.cs.meta new file mode 100644 index 0000000..c9a3292 --- /dev/null +++ b/Assets/_Project/Scripts/Dev/DebugConsole.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7c6a808e8291552d0b64e36a210879c4 \ No newline at end of file diff --git a/Assets/_Project/Scripts/UI/HUDController.cs b/Assets/_Project/Scripts/UI/HUDController.cs index 03cb3b0..badd2b8 100644 --- a/Assets/_Project/Scripts/UI/HUDController.cs +++ b/Assets/_Project/Scripts/UI/HUDController.cs @@ -157,8 +157,19 @@ namespace TD.UI // Set true whenever the chat input or any other text field on the HUD has // keyboard focus. Camera, builder input, and hotkey handlers all gate on - // this to keep typing from driving gameplay. - public static bool IsTextInputActive { get; private set; } + // this to keep typing from driving gameplay. Setter is internal (not + // private) so other standalone text-input surfaces — e.g. TD.Dev.DebugConsole, + // which is not part of HUDController — can contribute to the same flag + // instead of every gameplay script needing to know about every input widget. + public static bool IsTextInputActive { get; internal set; } + + // Frame until which chat's "Enter opens chat" behavior is suppressed. Other + // standalone text-input surfaces (e.g. TD.Dev.DebugConsole) set this when their + // own Enter-driven submit/close already consumed the keypress, so the same + // Enter press doesn't also get interpreted as "open chat" this frame. Without + // this, submitting a debug console command with Enter would immediately pop + // chat open too, since both systems poll the raw key independently. + internal static int SuppressChatOpenUntilFrame = -1; // ----- State ------------------------------------------------------ @@ -2017,7 +2028,7 @@ namespace TD.UI if (!chatInputOpen) { - if (enterDown) OpenChatInput(); + if (enterDown && Time.frameCount > SuppressChatOpenUntilFrame) OpenChatInput(); return; }