// Input Manager - Handles text input and option selection modes class InputManager { constructor(adapter) { this.adapter = adapter; this.mode = "idle"; // "idle" | "text" | "options" this.options = []; this.selectedIndex = 0; this.inputResolve = null; this.optionsContainerId = "game-options-" + Date.now(); this.keydownHandler = null; this._setupKeyboardListener(); } _setupKeyboardListener() { this.keydownHandler = (e) => { if (this.mode !== "options") return; // Check if terminal input has text - if so, let user submit commands like "quit" const terminalInput = document.getElementById("input"); const hasInputText = terminalInput && terminalInput.value.trim().length > 0; if (e.key === "ArrowUp") { e.preventDefault(); e.stopPropagation(); this._selectPrevious(); } else if (e.key === "ArrowDown") { e.preventDefault(); e.stopPropagation(); this._selectNext(); } else if (e.key === "Enter") { // If user has typed something, let it through to submit the command if (hasInputText) return; e.preventDefault(); e.stopPropagation(); this._confirmSelection(); } else if (/^[1-9]$/.test(e.key)) { // Only intercept if input is empty (user isn't typing a command) if (hasInputText) return; const index = parseInt(e.key) - 1; if (index < this.options.length) { e.preventDefault(); e.stopPropagation(); this.selectedIndex = index; this._confirmSelection(); } } }; document.addEventListener("keydown", this.keydownHandler, true); } // Text input mode - wait for user to type something awaitText(prompt = "") { return new Promise((resolve) => { if (prompt) { this.adapter.printInfo(prompt); } this.mode = "text"; this.inputResolve = resolve; this.adapter.captureInput((value) => { this.adapter.print(`> ${value}`); this.mode = "idle"; this.adapter.releaseInput(); if (this.inputResolve) { const res = this.inputResolve; this.inputResolve = null; res(value); } }); this.adapter.focusInput(); }); } // Options selection mode - display choices and wait for selection awaitOption(options, prompt = "") { return new Promise((resolve) => { this.mode = "options"; this.options = options; this.selectedIndex = 0; this.optionsContainerId = "game-options-" + Date.now(); if (prompt) { this.adapter.print(""); this.adapter.printInfo(prompt); } this.adapter.print(""); this._renderOptions(); this.inputResolve = (index) => { this.mode = "idle"; resolve({ index, option: this.options[index], }); }; }); } // Yes/No confirmation awaitConfirm(prompt = "Continue?") { return this.awaitOption( [ { text: "Yes", value: true }, { text: "No", value: false }, ], prompt, ).then((result) => result.option.value); } // Handle text input from game engine handleTextInput(value) { if (this.mode === "text" && this.inputResolve) { this.adapter.print(`> ${value}`); this.mode = "idle"; const res = this.inputResolve; this.inputResolve = null; res(value); } } _renderOptions() { const optionsHTML = this.options .map((opt, idx) => { const selected = idx === this.selectedIndex; const prefix = selected ? ">" : " "; const className = selected ? "game-option-selected" : "game-option"; const text = typeof opt === "string" ? opt : opt.text; return `
${prefix} ${idx + 1}. ${text}
`; }) .join(""); this.adapter.printHTML( `
${optionsHTML}
`, ); this.adapter.scrollToBottom(); } _updateOptionsDisplay() { const container = document.getElementById(this.optionsContainerId); if (!container) return; const optionDivs = container.querySelectorAll("div"); optionDivs.forEach((div, idx) => { const selected = idx === this.selectedIndex; div.className = selected ? "game-option-selected" : "game-option"; const text = typeof this.options[idx] === "string" ? this.options[idx] : this.options[idx].text; div.textContent = `${selected ? ">" : " "} ${idx + 1}. ${text}`; }); } _selectPrevious() { if (this.selectedIndex > 0) { this.selectedIndex--; this._updateOptionsDisplay(); } } _selectNext() { if (this.selectedIndex < this.options.length - 1) { this.selectedIndex++; this._updateOptionsDisplay(); } } _confirmSelection() { if (this.inputResolve) { const res = this.inputResolve; this.inputResolve = null; // Show what was selected const selectedOpt = this.options[this.selectedIndex]; const text = typeof selectedOpt === "string" ? selectedOpt : selectedOpt.text; this.adapter.print(""); this.adapter.printSuccess(`> ${text}`); res(this.selectedIndex); } } // Check if currently waiting for input isWaiting() { return this.mode !== "idle"; } // Get current mode getMode() { return this.mode; } // Cancel current input (for cleanup) cancel() { this.mode = "idle"; this.inputResolve = null; this.adapter.releaseInput(); } // Cleanup when game ends destroy() { if (this.keydownHandler) { document.removeEventListener("keydown", this.keydownHandler, true); } this.cancel(); } }