// 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; } } }