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