您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork镜像 is available in English.
Automates combining a target element with all others in Infinite Craft, remembering failed combinations to speed up runs.
// ==UserScript== // @name Infinite Craft - Auto Combiner V2 (with Fail Memory) // @namespace http://tampermonkey.net/ // @version 2.1 // @description Automates combining a target element with all others in Infinite Craft, remembering failed combinations to speed up runs. // @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 (Verify with DevTools if game updates) itemSelector: '.item', // Selector for draggable items // itemTextAttribute: NO LONGER USED - uses textContent now gameContainerSelector: '.container.main-container', // More specific container for observer // 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', // setPositionButtonId: REMOVED clearFailedButtonId: 'auto-combo-clear-failed-button', debugMarkerClass: 'auto-combo-debug-marker', // Still useful for visualizing drag path // Delays (ms) - Tune these based on game responsiveness interComboDelay: 100, // Delay between trying different combinations postComboScanDelay: 650, // Delay AFTER a combo attempt BEFORE checking result dragStepDelay: 15, // Delay between mouse events during a single drag // postDragDelay: REMOVED (only one drag per combo now) scanDebounceDelay: 300, // Delay before rescanning items after DOM changes suggestionHighlightDelay: 50, // Behavior suggestionLimit: 20, debugMarkerDuration: 1000, // Shorter duration might be less intrusive // Keys keyArrowUp: 'ArrowUp', keyArrowDown: 'ArrowDown', keyEnter: 'Enter', keyTab: 'Tab', // Storage (V2 to avoid conflicts if you used older versions) // storageKeyCoords: REMOVED storageKeyFailedCombos: 'infCraftAutoComboFailedCombosV2', // Styling & Z-Index panelZIndex: 10010, // Slightly higher Z-index just in case suggestionZIndex: 10011, markerZIndex: 10012, }; // --- CORE CLASS --- class AutoTargetCombo { constructor() { console.log('[AutoCombo] Initializing for Infinite Craft...'); this.itemElementMap = new Map(); // Map<string, Element> // this.manualBaseCoords = null; // REMOVED // this.awaitingClick = false; // REMOVED // this.baseReady = false; // REMOVED (no separate drop point needed) 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.setPositionButton = null; // REMOVED this.clearFailedButton = null; // --- 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) { throw new Error("One or more critical UI elements missing after setup. Aborting."); } this._setupEventListeners(); this._loadFailedCombos(); // Load saved failures 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: 15px; left: 15px; z-index: ${CONFIG.panelZIndex}; background: rgba(250, 250, 250, 0.97); padding: 12px; border: 1px solid #aaa; border-radius: 8px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 14px; width: 260px; /* Slightly narrower */ color: #111; box-shadow: 0 5px 15px rgba(0,0,0,0.25); display: flex; flex-direction: column; gap: 6px; /* Increased gap slightly */ } #${CONFIG.panelId} * { box-sizing: border-box; } #${CONFIG.panelId} div:first-child { /* Title */ font-weight: bold; margin-bottom: 4px; text-align: center; color: #333; font-size: 15px; padding-bottom: 4px; border-bottom: 1px solid #ddd; } #${CONFIG.panelId} input, #${CONFIG.panelId} button { width: 100%; padding: 9px 10px; font-size: 14px; 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; } #${CONFIG.panelId} button:hover { background-color: #e0e0e0; } #${CONFIG.panelId} button:active { transform: scale(0.98); } /* Specific Button Styles */ #${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; } /* setPositionButton styles removed */ #${CONFIG.panelId} #${CONFIG.clearFailedButtonId} { background-color: #ff9800; color: white; border-color: #c67600;} #${CONFIG.panelId} #${CONFIG.clearFailedButtonId}:hover { background-color: #f57c00; } #${CONFIG.suggestionBoxId} { display: none; border: 1px solid #aaa; background: #fff; position: absolute; max-height: 160px; overflow-y: auto; z-index: ${CONFIG.suggestionZIndex}; box-shadow: 0 4px 8px rgba(0,0,0,0.2); border-radius: 0 0 4px 4px; margin-top: -1px; /* Align with input */ font-size: 13px; } .${CONFIG.suggestionItemClass} { padding: 7px 12px; 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: 6px; color: #333; font-weight: 500; font-size: 13px; text-align: center; padding: 7px; background-color: #f9f9f9; border-radius: 3px; border: 1px solid #e5e5e5; } /* Status styles (unchanged) */ #${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: 10px; height: 10px; border-radius: 50%; z-index: ${CONFIG.markerZIndex}; pointer-events: none; opacity: 0.80; box-shadow: 0 0 5px 2px rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.5); transition: opacity 0.5s ease-out; /* Fade out */ } `; 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; // Removed Set Drop Position button from HTML this.panel.innerHTML = ` <div>✨ Infinite Auto Combiner ✨</div> <input id="${CONFIG.targetInputId}" placeholder="Target Element (e.g. Water)" autocomplete="off"> <div id="${CONFIG.suggestionBoxId}"></div> <button id="${CONFIG.startButtonId}">▶️ Combine with All Others</button> <button id="${CONFIG.clearFailedButtonId}">🗑️ Clear Failed Memory</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.setPositionButton = null; // REMOVED this.clearFailedButton = this.panel.querySelector(`#${CONFIG.clearFailedButtonId}`); // Log check (removed position button check) 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 }); // Crucial Check (removed position button check) if (!this.targetInput || !this.statusBox || !this.startButton || !this.stopButton || !this.clearFailedButton) { throw new Error("One or more required UI elements not found within the panel."); } } _setupEventListeners() { if (!this.targetInput || !this.startButton || !this.stopButton || !this.clearFailedButton) { 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(); // setPositionButton listener REMOVED this.clearFailedButton.onclick = () => this._clearFailedCombos(); // Canvas click listener REMOVED (no longer needed) } _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} failed combos.`, '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('❌ Error saving fails!', 'status-error'); } } _clearFailedCombos() { const count = this.failedCombos.size; this.failedCombos.clear(); localStorage.removeItem(CONFIG.storageKeyFailedCombos); this.logStatus(`🗑️ Cleared ${count} failed combos.`, 'status-success'); console.log(`[AutoCombo] Cleared ${count} failed combos.`); } // --- 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 || typeof el.textContent !== 'string') continue; // *** Use textContent for Infinite Craft *** const name = el.textContent.trim(); if (name) { // Ensure name is not empty after trimming 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 = `🔍 Scan: ${newSize} items`; if (diff > 0) logMsg += ` (+${diff})`; else if (diff < 0) logMsg += ` (${diff})`; this.logStatus(logMsg); console.log(`[AutoCombo] ${logMsg}`); // Update suggestions if input is focused if (document.activeElement === this.targetInput) { this._updateSuggestions(); } } return changed; } observeDOM() { // Use the configured game container selector const targetNode = document.querySelector(CONFIG.gameContainerSelector); if (!targetNode) { console.error("[AutoCombo] Cannot observe DOM: Target node not found:", CONFIG.gameContainerSelector); this.logStatus(`❌ Error: Observer target (${CONFIG.gameContainerSelector}) not found!`, "status-error"); // Fallback to body? Maybe not ideal if body is too broad. // targetNode = document.body; 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; // Maybe check if node *contains* an item selector too? More expensive. // if (node.nodeType === Node.ELEMENT_NODE && node.querySelector && node.querySelector(CONFIG.itemSelector)) return true; } return false; } if (checkNodes(mutation.addedNodes) || checkNodes(mutation.removedNodes)) { potentiallyRelevantChange = true; break; } } // No attribute watching needed for textContent changes } if (potentiallyRelevantChange) { clearTimeout(this.scanDebounceTimer); this.scanDebounceTimer = setTimeout(() => { // console.log("[AutoCombo] DOM change detected, rescanning items..."); // Can be noisy this.scanItems(); }, CONFIG.scanDebounceDelay); } }); observer.observe(targetNode, { childList: true, subtree: true, // Need subtree as items are nested // attributes: false // Not watching attributes anymore }); console.log("[AutoCombo] DOM Observer started on:", targetNode); } stop() { if (!this.isRunning) return; this.isRunning = false; clearTimeout(this.scanDebounceTimer); this.logStatus('⛔ Combo process stopped.', 'status-stopped'); console.log('[AutoCombo] Stop requested.'); } async startAutoCombo() { if (this.isRunning) { this.logStatus('⚠️ Already running.', 'status-warning'); return; } // No baseReady check needed const targetName = this.targetInput.value.trim(); if (!targetName) { this.logStatus('⚠️ Enter Target Element', 'status-warning'); this.targetInput.focus(); return; } this.scanItems(); // Ensure map is fresh let targetElement = this.getElement(targetName); if (!targetElement || !document.body.contains(targetElement)) { this.logStatus(`⚠️ Target "${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 other items found to combine with "${targetName}".`); return; } this.isRunning = true; this.logStatus(`🚀 Starting... Target: ${targetName} (${itemsToProcess.length} others)`, '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 known fails... ${progress}`, 'status-running'); } console.log(`[AutoCombo] ${progress} Skipping known fail: ${targetName} + ${itemName}`); skippedCount++; await new Promise(res => setTimeout(res, 2)); // Minimal delay continue; } // Re-get target each time, it might have been recreated targetElement = this.getElement(targetName); if (!targetElement || !document.body.contains(targetElement)) { this.logStatus(`⛔ Target "${targetName}" lost! Stopping.`, '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(`⚠️ Skipping "${itemName}" ${progress}: Elem lost.`, 'status-warning'); console.warn(`[AutoCombo] ${progress} Skipping "${itemName}": Element not found/removed.`); continue; } this.logStatus(`⏳ Trying: ${targetName} + ${itemName} ${progress}`, 'status-running'); console.log(`[AutoCombo] ${progress} Attempting: ${targetName} + ${itemName}`); attemptedCount++; const itemsBeforeCombo = new Set(this.itemElementMap.keys()); try { // *** Simulate Drag Source Onto Target *** await this.simulateCombo(sourceElement, targetElement); if (!this.isRunning) break; await new Promise(res => setTimeout(res, CONFIG.postComboScanDelay)); if (!this.isRunning) break; // *** Check Result *** // console.log("[AutoCombo] Explicitly rescanning after combo delay..."); // Debug this.scanItems(); // Force scan to update map 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(`✨ NEW: ${newItemFound}! (${targetName} + ${itemName})`, 'status-success'); console.log(`[AutoCombo] SUCCESS! New: ${newItemFound} from ${targetName} + ${itemName}`); // No need to check if target was consumed, as scanItems will update map anyway // and we re-get targetElement at the start of the loop. } else { const targetStillExists = itemsAfterCombo.has(targetName); const sourceStillExists = itemsAfterCombo.has(itemName); this.logStatus(`❌ Failed: ${targetName} + ${itemName} (T:${targetStillExists}, S:${sourceStillExists})`, '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 combining ${itemName}: ${error.message}`, 'status-error'); console.error(`[AutoCombo] Error during combo for "${itemName}" + "${targetName}":`, error); // Decide if stop is needed // this.stop(); break; } } // End loop if (this.isRunning) { this.isRunning = false; const summary = `✅ Done. Tried: ${attemptedCount}, New: ${successCount}, Skipped: ${skippedCount}.`; this.logStatus(summary, 'status-success'); console.log(`[AutoCombo] ${summary}`); } else { const summary = `⛔ Stopped. Tried: ${attemptedCount}, New: ${successCount}, Skipped: ${skippedCount}.`; console.log(`[AutoCombo] ${summary}`); } } // --- Simulation (Modified for Infinite Craft) --- async simulateCombo(sourceElement, targetElement) { if (!this.isRunning) return; // *** Get Target Position *** const targetRect = targetElement.getBoundingClientRect(); if (targetRect.width === 0 || targetRect.height === 0) { throw new Error(`Target "${targetElement.textContent.trim()}" has no size.`); } // Calculate center of the target element relative to the viewport const dropClientX = targetRect.left + targetRect.width / 2; const dropClientY = targetRect.top + targetRect.height / 2; // Convert to absolute document coordinates for potential markers (though less crucial now) const dropAbsoluteX = dropClientX + window.scrollX; const dropAbsoluteY = dropClientY + window.scrollY; // *** Simulate Dragging Source Onto Target Center *** await this.simulateDrag(sourceElement, dropAbsoluteX, dropAbsoluteY, 'rgba(50, 150, 255, 0.7)'); // Single drag } async simulateDrag(element, dropAbsoluteX, dropAbsoluteY, 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.textContent.trim()}" has no size.`); } const clientStartX = rect.left + rect.width / 2; const clientStartY = rect.top + rect.height / 2; // Convert absolute drop coordinates to client coords for events const clientDropX = dropAbsoluteX - window.scrollX; const clientDropY = dropAbsoluteY - window.scrollY; // Show markers at absolute positions (optional) this.showDebugMarker(clientStartX + window.scrollX, clientStartY + window.scrollY, markerColor); this.showDebugMarker(dropAbsoluteX, dropAbsoluteY, markerColor); try { // Mouse Down on Source Element element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, button: 0, clientX: clientStartX, clientY: clientStartY, buttons: 1 })); await new Promise(res => setTimeout(res, CONFIG.dragStepDelay)); if (!this.isRunning) return; // Mouse Move to Target Element Center document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, view: window, clientX: clientDropX, clientY: clientDropY, movementX: clientDropX - clientStartX, movementY: clientDropY - clientStartY, buttons: 1 })); await new Promise(res => setTimeout(res, CONFIG.dragStepDelay)); if (!this.isRunning) return; // Mouse Up (Dispatch on document works for Infinite Craft) document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, button: 0, clientX: clientDropX, clientY: clientDropY, buttons: 0 })); console.log(`[AutoCombo] Dragged ${element.textContent.trim()} onto approx (${Math.round(clientDropX)}, ${Math.round(clientDropY)})`); } catch (error) { console.error('[AutoCombo] Error during drag simulation step:', error); throw new Error(`Drag sim failed: ${error.message}`); } } // --- UI & Suggestions (Mostly Unchanged) --- _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: '160px', 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(); // Includes scroll into view } _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(); // Maybe auto-start here? e.g.: setTimeout(() => this.startAutoCombo(), 50); } // --- Event Handlers (Removed Canvas Click) --- // _handleCanvasClick REMOVED // --- 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 - 5}px`, left: `${x - 5}px`, backgroundColor: color, position: 'absolute', opacity: '0.8' }); document.body.appendChild(dot); setTimeout(() => { dot.style.opacity = '0'; // Start fade out setTimeout(() => dot.remove(), 500); // Remove after fade }, duration - 500); // Start fading before full duration } logStatus(msg, type = 'info') { if (!this.statusBox) { console.log('[AutoCombo Status]', msg); return; } this.statusBox.textContent = msg; this.statusBox.className = `${CONFIG.statusBoxId}`; // Reset classes 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 --- // Basic check to prevent multiple instances if script injected twice if (window.infCraftAutoComboInstance) { console.warn("[AutoCombo] Instance already running. Aborting new init."); } else { console.log("[AutoCombo] Creating new instance..."); // document-idle should mean DOM is ready, no need for DOMContentLoaded check window.infCraftAutoComboInstance = new AutoTargetCombo(); } })();