您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automates combining a target element with all others in Infinite Craft, remembering failed combinations to speed up runs. Mobile-friendly UI.
// ==UserScript== // @name Infinite Craft - Auto Combiner V3 (Mobile Optimized) // @namespace http://tampermonkey.net/ // @version 3 // @description Automates combining a target element with all others in Infinite Craft, remembering failed combinations to speed up runs. Mobile-friendly UI. // @author YourName (or Generated) // @match https://neal.fun/infinite-craft/ // @grant none // @run-at document-idle // @license MIT // ==/UserScript== (() => { // --- CONFIGURATION --- (Adjust delays if needed) const CONFIG = { // Selectors specific to Infinite Craft (FIXED) itemSelector: '.items .item', // FIXED: Was '.item', now '.items .item' gameContainerSelector: '.container', // Simplified // UI Element IDs & Classes panelId: 'auto-combo-panel', targetInputId: 'auto-combo-target-input', suggestionBoxId: 'auto-combo-suggestion-box', suggestionItemClass: 'auto-combo-suggestion-item', statusBoxId: 'auto-combo-status', startButtonId: 'auto-combo-start-button', stopButtonId: 'auto-combo-stop-button', clearFailedButtonId: 'auto-combo-clear-failed-button', speedSelectId: 'auto-combo-speed-select', debugMarkerClass: 'auto-combo-debug-marker', // Speed Presets (all values in ms) speedPresets: { slow: { interComboDelay: 150, postComboScanDelay: 800, dragStepDelay: 20, dragBetweenDelay: 250 }, normal: { interComboDelay: 100, postComboScanDelay: 650, dragStepDelay: 15, dragBetweenDelay: 200 }, fast: { interComboDelay: 50, postComboScanDelay: 500, dragStepDelay: 10, dragBetweenDelay: 150 }, turbo: { interComboDelay: 25, postComboScanDelay: 350, dragStepDelay: 5, dragBetweenDelay: 100 } }, // Default delays (will be overridden by speed selection) interComboDelay: 100, postComboScanDelay: 650, dragStepDelay: 15, dragBetweenDelay: 200, scanDebounceDelay: 300, suggestionHighlightDelay: 50, // Behavior suggestionLimit: 20, debugMarkerDuration: 1000, // Keys keyArrowUp: 'ArrowUp', keyArrowDown: 'ArrowDown', keyEnter: 'Enter', keyTab: 'Tab', // Storage storageKeyFailedCombos: 'infCraftAutoComboFailedCombosV2', storageKeySpeed: 'infCraftAutoComboSpeed', // Styling & Z-Index panelZIndex: 10010, suggestionZIndex: 10011, markerZIndex: 10012, }; // --- CORE CLASS --- class AutoTargetCombo { constructor() { console.log('[AutoCombo] Initializing for Infinite Craft...'); this.itemElementMap = new Map(); // Map<string, Element> this.isRunning = false; this.suggestionIndex = -1; this.suggestions = []; this.scanDebounceTimer = null; this.failedCombos = new Set(); // UI References this.panel = null; this.targetInput = null; this.suggestionBox = null; this.statusBox = null; this.startButton = null; this.stopButton = null; this.clearFailedButton = null; this.speedSelect = null; this.currentSpeed = 'normal'; // --- Initialization Steps --- try { this._injectStyles(); this._setupUI(); // Check if essential UI elements were found after setup if (!this.panel || !this.targetInput || !this.statusBox || !this.startButton || !this.stopButton || !this.clearFailedButton || !this.speedSelect) { throw new Error("One or more critical UI elements missing after setup. Aborting."); } this._setupEventListeners(); this._loadFailedCombos(); // Load saved failures this._loadSpeed(); // Load saved speed setting this.observeDOM(); this.scanItems(); // Perform initial scan this.logStatus('Ready.'); console.log('[AutoCombo] Initialization complete.'); } catch (error) { console.error('[AutoCombo] CRITICAL ERROR during initialization:', error); this.logStatus(`❌ INIT FAILED: ${error.message}`, 'status-error'); // Clean up partial UI if needed if (this.panel && this.panel.parentNode) { this.panel.parentNode.removeChild(this.panel); } } } // --- Initialization & Setup --- _injectStyles() { if (document.getElementById(`${CONFIG.panelId}-styles`)) return; const css = ` #${CONFIG.panelId} { position: fixed; top: 8px; left: 8px; z-index: ${CONFIG.panelZIndex}; background: rgba(250, 250, 250, 0.97); padding: 8px; border: 1px solid #aaa; border-radius: 6px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 12px; width: 200px; color: #111; box-shadow: 0 3px 10px rgba(0,0,0,0.25); display: flex; flex-direction: column; gap: 5px; max-width: calc(100vw - 16px); } #${CONFIG.panelId} * { box-sizing: border-box; } #${CONFIG.panelId} div:first-child { font-weight: bold; margin-bottom: 2px; text-align: center; color: #333; font-size: 13px; padding-bottom: 3px; border-bottom: 1px solid #ddd; } #${CONFIG.panelId} input, #${CONFIG.panelId} button, #${CONFIG.panelId} select { width: 100%; padding: 6px 8px; font-size: 12px; border: 1px solid #ccc; border-radius: 4px; } #${CONFIG.panelId} input { background-color: #fff; color: #000; } #${CONFIG.panelId} button { cursor: pointer; background-color: #f0f0f0; color: #333; transition: background-color 0.2s ease, transform 0.1s ease; border: 1px solid #bbb; text-align: center; font-size: 11px; padding: 5px 6px; } #${CONFIG.panelId} button:hover { background-color: #e0e0e0; } #${CONFIG.panelId} button:active { transform: scale(0.98); } #${CONFIG.panelId} #${CONFIG.startButtonId} { background-color: #4CAF50; color: white; border-color: #3a8d3d;} #${CONFIG.panelId} #${CONFIG.startButtonId}:hover { background-color: #45a049; } #${CONFIG.panelId} #${CONFIG.stopButtonId} { background-color: #f44336; color: white; border-color: #c4302b;} #${CONFIG.panelId} #${CONFIG.stopButtonId}:hover { background-color: #da190b; } #${CONFIG.panelId} #${CONFIG.clearFailedButtonId} { background-color: #ff9800; color: white; border-color: #c67600;} #${CONFIG.panelId} #${CONFIG.clearFailedButtonId}:hover { background-color: #f57c00; } #${CONFIG.panelId} select { background-color: #fff; color: #333; cursor: pointer; font-size: 11px; padding: 5px 6px; } #${CONFIG.panelId} select:hover { background-color: #f8f8f8; } #${CONFIG.suggestionBoxId} { display: none; border: 1px solid #aaa; background: #fff; position: absolute; max-height: 120px; overflow-y: auto; z-index: ${CONFIG.suggestionZIndex}; box-shadow: 0 3px 6px rgba(0,0,0,0.2); border-radius: 0 0 4px 4px; margin-top: -1px; font-size: 11px; } .${CONFIG.suggestionItemClass} { padding: 5px 8px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #222; } .${CONFIG.suggestionItemClass}:hover { background-color: #f0f0f0; } .${CONFIG.suggestionItemClass}.highlighted { background-color: #007bff; color: white; } #${CONFIG.statusBoxId} { margin-top: 3px; color: #333; font-weight: 500; font-size: 10px; text-align: center; padding: 5px; background-color: #f9f9f9; border-radius: 3px; border: 1px solid #e5e5e5; line-height: 1.3; min-height: 26px; display: flex; align-items: center; justify-content: center; } #${CONFIG.statusBoxId}.status-running { color: #007bff; } #${CONFIG.statusBoxId}.status-stopped { color: #dc3545; } #${CONFIG.statusBoxId}.status-success { color: #28a745; } #${CONFIG.statusBoxId}.status-warning { color: #ffc107; text-shadow: 0 0 1px #aaa; } #${CONFIG.statusBoxId}.status-error { color: #dc3545; font-weight: bold; } .${CONFIG.debugMarkerClass} { position: absolute; width: 8px; height: 8px; border-radius: 50%; z-index: ${CONFIG.markerZIndex}; pointer-events: none; opacity: 0.80; box-shadow: 0 0 4px 1px rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.5); transition: opacity 0.5s ease-out; } @media (max-width: 480px) { #${CONFIG.panelId} { width: 180px; font-size: 11px; padding: 6px; gap: 4px; } #${CONFIG.panelId} input, #${CONFIG.panelId} button, #${CONFIG.panelId} select { padding: 5px 6px; font-size: 11px; } #${CONFIG.statusBoxId} { font-size: 9px; padding: 4px; min-height: 24px; } } `; const styleSheet = document.createElement("style"); styleSheet.id = `${CONFIG.panelId}-styles`; styleSheet.type = "text/css"; styleSheet.innerText = css; document.head.appendChild(styleSheet); } _setupUI() { const existingPanel = document.getElementById(CONFIG.panelId); if (existingPanel) existingPanel.remove(); this.panel = document.createElement('div'); this.panel.id = CONFIG.panelId; this.panel.innerHTML = ` <div>✨ Auto Combiner</div> <input id="${CONFIG.targetInputId}" placeholder="Target Element" autocomplete="off"> <div id="${CONFIG.suggestionBoxId}"></div> <select id="${CONFIG.speedSelectId}" title="Speed"> <option value="slow">🐌 Slow</option> <option value="normal" selected>⚡ Normal</option> <option value="fast">🚀 Fast</option> <option value="turbo">💨 Turbo</option> </select> <button id="${CONFIG.startButtonId}">▶️ Start</button> <button id="${CONFIG.clearFailedButtonId}">🗑️ Clear Fails</button> <button id="${CONFIG.stopButtonId}">⛔ Stop</button> <div id="${CONFIG.statusBoxId}">Initializing...</div> `; document.body.appendChild(this.panel); // Get references using panel.querySelector this.targetInput = this.panel.querySelector(`#${CONFIG.targetInputId}`); this.suggestionBox = this.panel.querySelector(`#${CONFIG.suggestionBoxId}`); this.statusBox = this.panel.querySelector(`#${CONFIG.statusBoxId}`); this.startButton = this.panel.querySelector(`#${CONFIG.startButtonId}`); this.stopButton = this.panel.querySelector(`#${CONFIG.stopButtonId}`); this.clearFailedButton = this.panel.querySelector(`#${CONFIG.clearFailedButtonId}`); this.speedSelect = this.panel.querySelector(`#${CONFIG.speedSelectId}`); console.log('[AutoCombo] UI Element References:', { panel: !!this.panel, targetInput: !!this.targetInput, suggestionBox: !!this.suggestionBox, statusBox: !!this.statusBox, startButton: !!this.startButton, stopButton: !!this.stopButton, clearFailedButton: !!this.clearFailedButton, speedSelect: !!this.speedSelect }); if (!this.targetInput || !this.statusBox || !this.startButton || !this.stopButton || !this.clearFailedButton || !this.speedSelect) { throw new Error("One or more required UI elements not found within the panel."); } } _setupEventListeners() { if (!this.targetInput || !this.startButton || !this.stopButton || !this.clearFailedButton || !this.speedSelect) { throw new Error("Cannot setup listeners: Required UI elements missing."); } this.targetInput.addEventListener('input', () => this._updateSuggestions()); this.targetInput.addEventListener('keydown', e => this._handleSuggestionKey(e)); this.targetInput.addEventListener('focus', () => this._updateSuggestions()); document.addEventListener('click', (e) => { if (this.panel && !this.panel.contains(e.target) && this.suggestionBox && !this.suggestionBox.contains(e.target)) { if (this.suggestionBox.style.display === 'block') this.suggestionBox.style.display = 'none'; } }, true); this.startButton.onclick = () => this.startAutoCombo(); this.stopButton.onclick = () => this.stop(); this.clearFailedButton.onclick = () => this._clearFailedCombos(); this.speedSelect.onchange = () => this._handleSpeedChange(); } _loadFailedCombos() { const savedCombos = localStorage.getItem(CONFIG.storageKeyFailedCombos); let loadedCount = 0; if (savedCombos) { try { const parsedCombos = JSON.parse(savedCombos); if (Array.isArray(parsedCombos)) { const validCombos = parsedCombos.filter(item => typeof item === 'string'); this.failedCombos = new Set(validCombos); loadedCount = this.failedCombos.size; if (loadedCount > 0) { this.logStatus(`📚 Loaded ${loadedCount} fails`, 'status-success'); } } else { localStorage.removeItem(CONFIG.storageKeyFailedCombos); this.failedCombos = new Set(); } } catch (e) { console.error('[AutoCombo] Error parsing failed combos:', e); localStorage.removeItem(CONFIG.storageKeyFailedCombos); this.failedCombos = new Set(); } } else { this.failedCombos = new Set(); } console.log(`[AutoCombo] Failed combos loaded: ${loadedCount}`); } _saveFailedCombos() { if (this.failedCombos.size === 0) { localStorage.removeItem(CONFIG.storageKeyFailedCombos); return; } try { localStorage.setItem(CONFIG.storageKeyFailedCombos, JSON.stringify(Array.from(this.failedCombos))); } catch (e) { console.error('[AutoCombo] Error saving failed combos:', e); this.logStatus('❌ Save error!', 'status-error'); } } _clearFailedCombos() { const count = this.failedCombos.size; this.failedCombos.clear(); localStorage.removeItem(CONFIG.storageKeyFailedCombos); this.logStatus(`🗑️ Cleared ${count} fails`, 'status-success'); console.log(`[AutoCombo] Cleared ${count} failed combos.`); } _loadSpeed() { const savedSpeed = localStorage.getItem(CONFIG.storageKeySpeed); if (savedSpeed && CONFIG.speedPresets[savedSpeed]) { this.currentSpeed = savedSpeed; } else { this.currentSpeed = 'normal'; } if (this.speedSelect) { this.speedSelect.value = this.currentSpeed; } this._applySpeed(); console.log(`[AutoCombo] Speed loaded: ${this.currentSpeed}`); } _handleSpeedChange() { if (!this.speedSelect) return; this.currentSpeed = this.speedSelect.value; localStorage.setItem(CONFIG.storageKeySpeed, this.currentSpeed); this._applySpeed(); this.logStatus(`⚙️ ${this.currentSpeed.toUpperCase()}`, 'status-success'); console.log(`[AutoCombo] Speed changed to: ${this.currentSpeed}`); } _applySpeed() { const preset = CONFIG.speedPresets[this.currentSpeed]; if (!preset) { console.warn(`[AutoCombo] Invalid speed preset: ${this.currentSpeed}, using normal`); this.currentSpeed = 'normal'; return this._applySpeed(); } CONFIG.interComboDelay = preset.interComboDelay; CONFIG.postComboScanDelay = preset.postComboScanDelay; CONFIG.dragStepDelay = preset.dragStepDelay; CONFIG.dragBetweenDelay = preset.dragBetweenDelay; } // --- Core Logic --- scanItems() { clearTimeout(this.scanDebounceTimer); this.scanDebounceTimer = null; const items = document.querySelectorAll(CONFIG.itemSelector); let changed = false; const currentNames = new Set(); const oldSize = this.itemElementMap.size; for (const el of items) { if (!el) continue; // Use data-item-text attribute for clean element name const name = el.getAttribute('data-item-text'); if (name && typeof name === 'string') { currentNames.add(name); if (!this.itemElementMap.has(name) || this.itemElementMap.get(name) !== el) { this.itemElementMap.set(name, el); changed = true; } } } const currentKeys = Array.from(this.itemElementMap.keys()); for (const name of currentKeys) { if (!currentNames.has(name)) { this.itemElementMap.delete(name); changed = true; } } if (changed && !this.isRunning) { const newSize = this.itemElementMap.size; const diff = newSize - oldSize; let logMsg = `🔍 ${newSize} items`; if (diff > 0) logMsg += ` (+${diff})`; else if (diff < 0) logMsg += ` (${diff})`; this.logStatus(logMsg); console.log(`[AutoCombo] ${logMsg}`); if (document.activeElement === this.targetInput) { this._updateSuggestions(); } } return changed; } observeDOM() { const targetNode = document.querySelector(CONFIG.gameContainerSelector); if (!targetNode) { console.error("[AutoCombo] Cannot observe DOM: Target node not found:", CONFIG.gameContainerSelector); this.logStatus(`❌ Observer error!`, "status-error"); return; } const observer = new MutationObserver((mutationsList) => { let potentiallyRelevantChange = false; for (const mutation of mutationsList) { if (mutation.type === 'childList') { const checkNodes = (nodes) => { if (!nodes) return false; for(const node of nodes) { if (node.nodeType === Node.ELEMENT_NODE && node.matches && node.matches(CONFIG.itemSelector)) return true; } return false; } if (checkNodes(mutation.addedNodes) || checkNodes(mutation.removedNodes)) { potentiallyRelevantChange = true; break; } } } if (potentiallyRelevantChange) { clearTimeout(this.scanDebounceTimer); this.scanDebounceTimer = setTimeout(() => { this.scanItems(); }, CONFIG.scanDebounceDelay); } }); observer.observe(targetNode, { childList: true, subtree: true, }); console.log("[AutoCombo] DOM Observer started on:", targetNode); } stop() { if (!this.isRunning) return; this.isRunning = false; clearTimeout(this.scanDebounceTimer); this.logStatus('⛔ Stopped', 'status-stopped'); console.log('[AutoCombo] Stop requested.'); } async startAutoCombo() { if (this.isRunning) { this.logStatus('⚠️ Already running', 'status-warning'); return; } const targetName = this.targetInput.value.trim(); if (!targetName) { this.logStatus('⚠️ Enter Target', 'status-warning'); this.targetInput.focus(); return; } this.scanItems(); let targetElement = this.getElement(targetName); if (!targetElement || !document.body.contains(targetElement)) { this.logStatus(`⚠️ "${targetName}" not found`, 'status-warning'); this.targetInput.focus(); return; } const itemsToProcess = Array.from(this.itemElementMap.keys()).filter(name => name !== targetName); if (itemsToProcess.length === 0) { this.logStatus(`ℹ️ No items to combine`); return; } this.isRunning = true; this.logStatus(`🚀 Starting... (${itemsToProcess.length})`, 'status-running'); console.log(`[AutoCombo] Starting combinations for "${targetName}". Items: ${itemsToProcess.length}. Fails: ${this.failedCombos.size}`); let processedCount = 0, attemptedCount = 0, successCount = 0, skippedCount = 0; const totalPotentialCombos = itemsToProcess.length; for (const itemName of itemsToProcess) { if (!this.isRunning) break; processedCount++; const progress = `${processedCount}/${totalPotentialCombos}`; const comboKey = this._getComboKey(targetName, itemName); if (this.failedCombos.has(comboKey)) { if (processedCount % 20 === 0 || processedCount === totalPotentialCombos) { this.logStatus(`⏭️ Skipping... ${progress}`, 'status-running'); } console.log(`[AutoCombo] ${progress} Skipping known fail: ${targetName} + ${itemName}`); skippedCount++; await new Promise(res => setTimeout(res, 2)); continue; } targetElement = this.getElement(targetName); if (!targetElement || !document.body.contains(targetElement)) { this.logStatus(`⛔ Target lost!`, 'status-error'); console.error(`[AutoCombo] Target element "${targetName}" disappeared mid-process.`); this.isRunning = false; break; } const sourceElement = this.getElement(itemName); if (!sourceElement || !document.body.contains(sourceElement)) { this.logStatus(`⚠️ Skip ${progress}`, 'status-warning'); console.warn(`[AutoCombo] ${progress} Skipping "${itemName}": Element not found/removed.`); continue; } this.logStatus(`⏳ ${progress}`, 'status-running'); console.log(`[AutoCombo] ${progress} Attempting: ${targetName} + ${itemName}`); attemptedCount++; const itemsBeforeCombo = new Set(this.itemElementMap.keys()); try { await this.simulateCombo(sourceElement, targetElement); if (!this.isRunning) break; await new Promise(res => setTimeout(res, CONFIG.postComboScanDelay)); if (!this.isRunning) break; this.scanItems(); const itemsAfterCombo = new Set(this.itemElementMap.keys()); let newItemFound = null; for (const itemAfter of itemsAfterCombo) { if (!itemsBeforeCombo.has(itemAfter)) { newItemFound = itemAfter; break; } } if (newItemFound) { successCount++; this.logStatus(`✨ ${newItemFound}!`, 'status-success'); console.log(`[AutoCombo] SUCCESS! New: ${newItemFound} from ${targetName} + ${itemName}`); } else { const targetStillExists = itemsAfterCombo.has(targetName); const sourceStillExists = itemsAfterCombo.has(itemName); this.logStatus(`❌ Fail ${progress}`, 'status-running'); console.log(`[AutoCombo] FAILURE: No new item from ${targetName} + ${itemName}. T:${targetStillExists}, S:${sourceStillExists}`); this.failedCombos.add(comboKey); this._saveFailedCombos(); } await new Promise(res => setTimeout(res, CONFIG.interComboDelay)); } catch (error) { this.logStatus(`❌ Error ${progress}`, 'status-error'); console.error(`[AutoCombo] Error during combo for "${itemName}" + "${targetName}":`, error); } } if (this.isRunning) { this.isRunning = false; const summary = `✅ Done! New: ${successCount}`; this.logStatus(summary, 'status-success'); console.log(`[AutoCombo] Done. Tried: ${attemptedCount}, New: ${successCount}, Skipped: ${skippedCount}.`); } else { const summary = `⛔ Stopped. New: ${successCount}`; this.logStatus(summary, 'status-stopped'); console.log(`[AutoCombo] Stopped. Tried: ${attemptedCount}, New: ${successCount}, Skipped: ${skippedCount}.`); } } // --- Simulation --- async simulateCombo(sourceElement, targetElement) { if (!this.isRunning) return; // Get center of the canvas/screen for combining const centerX = Math.floor(window.innerWidth / 2); const centerY = Math.floor(window.innerHeight / 2); // Drag target element to center first (offset upward) await this.simulateDrag(targetElement, centerX, centerY - 30, 'rgba(255, 100, 50, 0.7)'); // Small delay between drags (using speed setting) await new Promise(res => setTimeout(res, CONFIG.dragBetweenDelay)); if (!this.isRunning) return; // Drag source element to center (offset downward) to combine await this.simulateDrag(sourceElement, centerX, centerY + 2, 'rgba(50, 150, 255, 0.7)'); } async simulateDrag(element, dropX, dropY, markerColor) { if (!this.isRunning || !element || !document.body.contains(element)) { console.warn("[AutoCombo] simulateDrag: Element invalid or drag stopped."); return; } const rect = element.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) { throw new Error(`Dragged elem "${element.getAttribute('data-item-text') || 'unknown'}" has no size.`); } // Starting position (center of element) const startX = rect.left + rect.width / 2; const startY = rect.top + rect.height / 2; // Show debug markers this.showDebugMarker(startX, startY, markerColor); this.showDebugMarker(dropX, dropY, markerColor); try { // Mouse Down on element element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: startX, clientY: startY })); await new Promise(res => setTimeout(res, CONFIG.dragStepDelay)); if (!this.isRunning) return; // Mouse Move to drop position document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: dropX, clientY: dropY })); await new Promise(res => setTimeout(res, CONFIG.dragStepDelay)); if (!this.isRunning) return; // Mouse Up at drop position document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: dropX, clientY: dropY })); console.log(`[AutoCombo] Dragged ${element.getAttribute('data-item-text') || 'unknown'} from (${Math.round(startX)}, ${Math.round(startY)}) to (${Math.round(dropX)}, ${Math.round(dropY)})`); } catch (error) { console.error('[AutoCombo] Error during drag simulation step:', error); throw new Error(`Drag sim failed: ${error.message}`); } } // --- UI & Suggestions --- _updateSuggestions() { if (!this.targetInput || !this.suggestionBox) return; const query = this.targetInput.value.toLowerCase(); if (!query) { this.suggestions = []; this.suggestionBox.style.display = 'none'; return; } const currentItems = Array.from(this.itemElementMap.keys()); this.suggestions = currentItems .filter(name => name.toLowerCase().includes(query)) .sort((a, b) => { const aI = a.toLowerCase().indexOf(query), bI = b.toLowerCase().indexOf(query); if (aI !== bI) return aI - bI; return a.localeCompare(b); }) .slice(0, CONFIG.suggestionLimit); this.suggestionIndex = -1; this._updateSuggestionUI(); } _updateSuggestionUI() { if (!this.targetInput || !this.suggestionBox) return; this.suggestionBox.innerHTML = ''; if (!this.suggestions.length) { this.suggestionBox.style.display = 'none'; return; } const inputRect = this.targetInput.getBoundingClientRect(); Object.assign(this.suggestionBox.style, { position: 'absolute', display: 'block', top: `${inputRect.bottom + window.scrollY}px`, left: `${inputRect.left + window.scrollX}px`, width: `${inputRect.width}px`, maxHeight: '120px', overflowY: 'auto', zIndex: CONFIG.suggestionZIndex, }); this.suggestions.forEach((name, index) => { const div = document.createElement('div'); div.textContent = name; div.className = CONFIG.suggestionItemClass; div.title = name; div.addEventListener('mousedown', (e) => { e.preventDefault(); this._handleSuggestionSelection(name); }); this.suggestionBox.appendChild(div); }); this._updateSuggestionHighlight(); } _handleSuggestionKey(e) { if (!this.suggestionBox || this.suggestionBox.style.display !== 'block' || !this.suggestions.length) { if (e.key === CONFIG.keyEnter) { e.preventDefault(); this.startAutoCombo(); } return; } const numSuggestions = this.suggestions.length; switch (e.key) { case CONFIG.keyArrowDown: case CONFIG.keyTab: e.preventDefault(); this.suggestionIndex = (this.suggestionIndex + 1) % numSuggestions; this._updateSuggestionHighlight(); break; case CONFIG.keyArrowUp: e.preventDefault(); this.suggestionIndex = (this.suggestionIndex - 1 + numSuggestions) % numSuggestions; this._updateSuggestionHighlight(); break; case CONFIG.keyEnter: e.preventDefault(); if (this.suggestionIndex >= 0) this._handleSuggestionSelection(this.suggestions[this.suggestionIndex]); else { this.suggestionBox.style.display = 'none'; this.startAutoCombo(); } break; case 'Escape': e.preventDefault(); this.suggestionBox.style.display = 'none'; break; } } _updateSuggestionHighlight() { if (!this.suggestionBox) return; Array.from(this.suggestionBox.children).forEach((child, i) => child.classList.toggle('highlighted', i === this.suggestionIndex)); this._scrollSuggestionIntoView(); } _scrollSuggestionIntoView() { if (!this.suggestionBox) return; const highlightedItem = this.suggestionBox.querySelector(`.${CONFIG.suggestionItemClass}.highlighted`); if (highlightedItem) { setTimeout(() => highlightedItem.scrollIntoView?.({ block: 'nearest' }), CONFIG.suggestionHighlightDelay); } } _handleSuggestionSelection(name) { if (!this.targetInput || !this.suggestionBox) return; this.targetInput.value = name; this.suggestionBox.style.display = 'none'; this.suggestions = []; this.targetInput.focus(); } // --- Utilities --- getElement(name) { return this.itemElementMap.get(name) || null; } showDebugMarker(x, y, color = 'red', duration = CONFIG.debugMarkerDuration) { const dot = document.createElement('div'); dot.className = CONFIG.debugMarkerClass; Object.assign(dot.style, { top: `${y - 4}px`, left: `${x - 4}px`, backgroundColor: color, position: 'absolute', opacity: '0.8' }); document.body.appendChild(dot); setTimeout(() => { dot.style.opacity = '0'; setTimeout(() => dot.remove(), 500); }, duration - 500); } logStatus(msg, type = 'info') { if (!this.statusBox) { console.log('[AutoCombo Status]', msg); return; } this.statusBox.textContent = msg; this.statusBox.className = `${CONFIG.statusBoxId}`; if (type !== 'info') this.statusBox.classList.add(`status-${type}`); if (type !== 'info' && type !== 'status-running' || !this.isRunning) { console.log(`[AutoCombo Status - ${type}]`, msg); } } _getComboKey(name1, name2) { return [name1, name2].sort().join('||'); } } // --- Initialization --- if (window.infCraftAutoComboInstance) { console.warn("[AutoCombo] Instance already running. Aborting new init."); } else { console.log("[AutoCombo] Creating new instance..."); window.infCraftAutoComboInstance = new AutoTargetCombo(); } })();