Infinite Craft - Auto Combiner V3 (Mobile Optimized)

Automates combining a target element with all others in Infinite Craft, remembering failed combinations to speed up runs. Mobile-friendly UI.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
    }

})();