Infinite Craft Auto Combo

Automates combining a target element with all other discovered elements in Infinite Craft.

// ==UserScript==
// @name         Infinite Craft Auto Combo
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Automates combining a target element with all other discovered elements in Infinite Craft.
// @author       Your Name (or Original Author if known) & Adapted by AI
// @match        https://neal.fun/infinite-craft/
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        // Selectors & Attributes (Infinite Craft specific)
        itemSelector: '.item', // Selector for draggable items in the game (sidebar)
        itemTextAttribute: 'data-item-text', // NOTE: Infinite Craft uses textContent, not this attribute. Kept for reference.
        gameContainerSelector: '.sidebar', // More specific container holding the discoverable items

        // 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: 'auto-combo-set-position-button',
        debugMarkerClass: 'auto-combo-debug-marker',

        // Delays (ms)
        comboLoopDelay: 100,      // Slightly increased delay for Infinite Craft stability
        dragStepDelay: 20,        // Slightly increased delay for Infinite Craft stability
        postDragDelay: 60,        // Slightly increased delay for Infinite Craft stability
        scanDebounceDelay: 350,   // Delay before rescanning items after DOM changes
        suggestionHighlightDelay: 50, // Delay for suggestion highlighting/scrolling

        // Behavior
        suggestionLimit: 20,      // Max number of suggestions to show
        debugMarkerDuration: 1500,// How long debug markers stay visible (ms)

        // Keys
        keyArrowUp: 'ArrowUp',
        keyArrowDown: 'ArrowDown',
        keyEnter: 'Enter',
        keyTab: 'Tab',

        // Storage
        storageKeyCoords: 'infiniteCraftAutoComboDropCoords', // Unique storage key

        // Styling & Z-Index
        panelZIndex: 10000,
        suggestionZIndex: 10001,
        markerZIndex: 10002,
    };

    class AutoTargetCombo {
        constructor() {
            // State
            this.itemElementMap = new Map(); // Map<string, Element>
            this.manualBaseCoords = null; // { x: number, y: number }
            this.awaitingClick = false;
            this.baseReady = false;
            this.isRunning = false;
            this.suggestionIndex = -1;
            this.suggestions = [];
            this.scanDebounceTimer = null;

            // UI Element References
            this.panel = null;
            this.targetInput = null;
            this.suggestionBox = null;
            this.statusBox = null;

            // Initial Setup
            this._injectStyles();
            this._setupUI();
            this._setupEventListeners();
            this._loadSettings(); // Try loading saved drop position
            this.observeDOM();
            this.scanItems(); // Initial scan
            this.logStatus("✅ Script loaded. Set Drop Position.");
        }

        // --- Initialization & Setup ---

        _injectStyles() {
            const css = `
                #${CONFIG.panelId} {
                    position: fixed; /* Use fixed to stay in view */
                    top: 10px; right: 10px; /* Position top-right */
                    z-index: ${CONFIG.panelZIndex};
                    background: #fff; padding: 12px; border: 2px solid #333; border-radius: 8px;
                    font-family: sans-serif; font-size: 14px; width: 260px; color: #000;
                    box-shadow: 0 4px 10px rgba(0,0,0,0.2);
                }
                #${CONFIG.panelId} input, #${CONFIG.panelId} button {
                    width: 100%; box-sizing: border-box; margin: 6px 0; padding: 8px; font-size: 14px;
                    border: 1px solid #ccc; border-radius: 4px;
                }
                 #${CONFIG.panelId} button {
                    cursor: pointer; background-color: #eee; transition: background-color 0.2s ease;
                 }
                 #${CONFIG.panelId} button:hover { background-color: #ddd; }
                 #${CONFIG.panelId} #${CONFIG.startButtonId} { background-color: #4CAF50; color: white; }
                 #${CONFIG.panelId} #${CONFIG.startButtonId}:hover { background-color: #45a049; }
                 #${CONFIG.panelId} #${CONFIG.stopButtonId} { background-color: #f44336; color: white; }
                 #${CONFIG.panelId} #${CONFIG.stopButtonId}:hover { background-color: #da190b; }
                 #${CONFIG.panelId} #${CONFIG.setPositionButtonId} { background-color: #008CBA; color: white; }
                 #${CONFIG.panelId} #${CONFIG.setPositionButtonId}:hover { background-color: #007ba7; }

                #${CONFIG.suggestionBoxId} {
                    display: none; border: 1px solid #ccc; background: #fff;
                    position: absolute; /* Position relative to panel */
                    max-height: 150px; overflow-y: auto;
                    z-index: ${CONFIG.suggestionZIndex}; box-shadow: 0 2px 5px rgba(0,0,0,0.15);
                    border-radius: 0 0 4px 4px;
                    width: 100%; /* Match panel width */
                    box-sizing: border-box;
                    left: 0; /* Align with left edge of input */
                    /* Top position calculated dynamically */
                }
                .${CONFIG.suggestionItemClass} {
                    padding: 6px 10px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
                }
                .${CONFIG.suggestionItemClass}:hover { background-color: #f0f0f0; }
                .${CONFIG.suggestionItemClass}.highlighted { background-color: #e0e0e0; }

                #${CONFIG.statusBoxId} { margin-top: 10px; color: #333; font-weight: bold; font-size: 13px; min-height: 1.2em;}
                .${CONFIG.debugMarkerClass} {
                    position: absolute; width: 8px; height: 8px; border-radius: 50%;
                    z-index: ${CONFIG.markerZIndex}; pointer-events: none; opacity: 0.8;
                    box-shadow: 0 0 3px 1px rgba(0,0,0,0.5);
                }
            `;
            const styleSheet = document.createElement("style");
            styleSheet.type = "text/css";
            styleSheet.innerText = css;
            document.head.appendChild(styleSheet);
        }

        _setupUI() {
            this.panel = document.createElement('div');
            this.panel.id = CONFIG.panelId;
            this.panel.innerHTML = `
                <div style="font-weight: bold; margin-bottom: 8px;">🎯 Auto Combo Target</div>
                <div style="position: relative;"> <!-- Wrapper for input + suggestions -->
                    <input id="${CONFIG.targetInputId}" placeholder="Target element (e.g. Fire)">
                    <div id="${CONFIG.suggestionBoxId}"></div>
                </div>
                <button id="${CONFIG.startButtonId}">▶️ Combine with Pool</button>
                <button id="${CONFIG.setPositionButtonId}">🖱️ Set Drop Position</button>
                <button id="${CONFIG.stopButtonId}">⛔ Stop</button>
                <div id="${CONFIG.statusBoxId}">Initializing...</div>
            `;
            document.body.appendChild(this.panel);

            this.targetInput = document.getElementById(CONFIG.targetInputId);
            this.suggestionBox = document.getElementById(CONFIG.suggestionBoxId);
            this.statusBox = document.getElementById(CONFIG.statusBoxId);

            // Set suggestion box top relative to input after UI is added
             const inputRect = this.targetInput.getBoundingClientRect();
             const panelRect = this.panel.getBoundingClientRect();
             // Calculate relative top position within the panel
             this.suggestionBox.style.top = `${inputRect.bottom - panelRect.top}px`;
        }

        _setupEventListeners() {
            this.targetInput.addEventListener('input', () => this._updateSuggestions());
            this.targetInput.addEventListener('keydown', e => this._handleSuggestionKey(e));
            this.targetInput.addEventListener('focus', () => this._updateSuggestions());

            // Close suggestions on outside click
            document.addEventListener('click', (e) => {
                 if (!this.panel.contains(e.target)) {
                    this.suggestionBox.style.display = 'none';
                }
            }, true); // Use capture phase

            document.getElementById(CONFIG.startButtonId).onclick = () => this.startAutoCombo();
            document.getElementById(CONFIG.stopButtonId).onclick = () => this.stop();
            document.getElementById(CONFIG.setPositionButtonId).onclick = () => {
                this.awaitingClick = true;
                this.logStatus('🖱️ Click anywhere on the main game area (not sidebar) to set drop target...');
                 // Add visual feedback that we are waiting
                 document.body.style.cursor = 'crosshair';
            };

            // Listener for setting drop position
            document.addEventListener('click', this._handleCanvasClick.bind(this), true); // Use capture to potentially override game behavior
        }

        _loadSettings() {
            const savedCoords = localStorage.getItem(CONFIG.storageKeyCoords);
            if (savedCoords) {
                try {
                    this.manualBaseCoords = JSON.parse(savedCoords);
                    if (this.manualBaseCoords && typeof this.manualBaseCoords.x === 'number' && typeof this.manualBaseCoords.y === 'number') {
                        this.baseReady = true;
                        this.logStatus('📍 Drop point loaded. Ready.');
                        this.showDebugMarker(this.manualBaseCoords.x, this.manualBaseCoords.y, 'purple', 2500); // Mark loaded pos
                    } else {
                         this.manualBaseCoords = null; // Invalid data
                         this.logStatus('⚠️ Invalid stored position. Please set.');
                    }
                } catch (e) {
                    console.error('[AutoCombo] Error loading saved coordinates:', e);
                    localStorage.removeItem(CONFIG.storageKeyCoords); // Clear invalid data
                     this.logStatus('⚠️ Error loading position. Please set.');
                }
            } else {
                 this.logStatus('ℹ️ Click "Set Drop Position" first.');
            }
        }

        // --- Core Logic ---

        scanItems() {
            // Clear previous scan timeout if any
            if (this.scanDebounceTimer) {
                clearTimeout(this.scanDebounceTimer);
                this.scanDebounceTimer = null; // Reset timer ID
            }

            const items = document.querySelectorAll(CONFIG.itemSelector);
            let changed = false;
            const currentNames = new Set();
            const tempMap = new Map(); // Use a temporary map to build the new state

            for (const el of items) {
                // Infinite Craft Specific: Get text content, trim whitespace
                const name = el.textContent?.trim();
                if (name && name !== "New Discovery") { // Make sure it's a valid item name
                    currentNames.add(name);
                    // Add if new, or update element reference if it changed
                     // Always update the element reference in case the element was re-rendered
                    tempMap.set(name, el);
                    if (!this.itemElementMap.has(name) || this.itemElementMap.get(name) !== el) {
                        changed = true;
                    }
                }
            }

            // Check for deletions
            if (this.itemElementMap.size !== currentNames.size) {
                 changed = true;
            }

            // Update the main map
            this.itemElementMap = tempMap;

            if (changed && !this.isRunning) { // Only log scan changes when not actively running
                 this.logStatus(`🔍 Scanned ${this.itemElementMap.size} unique items.`, true);
                 // Re-filter suggestions if the input has focus and text
                 if (document.activeElement === this.targetInput && this.targetInput.value) {
                    this._updateSuggestions();
                 }
            }
        }

        observeDOM() {
            const targetNode = document.querySelector(CONFIG.gameContainerSelector);
             if (!targetNode) {
                 console.error("[AutoCombo] Could not find target container:", CONFIG.gameContainerSelector);
                 this.logStatus("❌ Error: Cannot find game container!");
                 return;
             }

            const observer = new MutationObserver((mutationsList) => {
                 // Basic check if relevant nodes were added/removed or text changed
                 let potentiallyRelevantChange = false;
                 for (const mutation of mutationsList) {
                     if (mutation.type === 'childList') {
                         // Check if added/removed nodes could be items (simple check)
                         const checkNodes = (nodes) => {
                            for(const node of nodes) {
                                if (node.nodeType === Node.ELEMENT_NODE && node.matches && node.matches(CONFIG.itemSelector)) return true;
                                if (node.nodeType === Node.TEXT_NODE && node.parentElement?.matches(CONFIG.itemSelector)) return true; // Text change within item
                                if (node.nodeType === Node.ELEMENT_NODE && node.querySelector && node.querySelector(CONFIG.itemSelector)) return true; // Item added inside a fragment
                            }
                            return false;
                         }
                         if (checkNodes(mutation.addedNodes) || checkNodes(mutation.removedNodes)) {
                            potentiallyRelevantChange = true;
                            break;
                         }
                     } else if (mutation.type === 'characterData') { // Detect text changes directly
                         if (mutation.target.parentElement?.matches(CONFIG.itemSelector)) {
                             potentiallyRelevantChange = true;
                             break;
                         }
                     }
                 }

                // If relevant changes occurred, debounce the scan
                if (potentiallyRelevantChange) {
                    if (this.scanDebounceTimer) clearTimeout(this.scanDebounceTimer);
                    this.scanDebounceTimer = setTimeout(() => this.scanItems(), CONFIG.scanDebounceDelay);
                }
            });

            observer.observe(targetNode, {
                childList: true,
                subtree: true,
                characterData: true // Observe text changes within the container subtree
             });
             console.log("[AutoCombo] Observer attached to:", targetNode);
        }

        stop() {
            this.isRunning = false; // Set flag first
            // Any cleanup needed for ongoing simulation steps can be added here if necessary
            this.logStatus('⛔ Auto combo stopped.');
        }

        async startAutoCombo() {
            if (this.isRunning) {
                return this.logStatus('⚠️ Already running.');
            }
            if (!this.baseReady || !this.manualBaseCoords) {
                return this.logStatus('⚠️ Set drop position first!');
            }

            const targetName = this.targetInput.value.trim();
            if (!targetName) {
                return this.logStatus('⚠️ Enter a target element name.');
            }

            // Rescan right before starting to ensure map is fresh
            this.scanItems();

            let targetElement = this.getElement(targetName);
            if (!targetElement) {
                return this.logStatus(`⚠️ Target "${targetName}" not found in sidebar.`);
            }
             if (!document.body.contains(targetElement)) {
                 this.scanItems(); // Rescan if element is stale
                 targetElement = this.getElement(targetName);
                 if (!targetElement) return this.logStatus(`⚠️ Target "${targetName}" not found after rescan.`);
            }


            const itemsToProcess = Array.from(this.itemElementMap.keys()).filter(name => name !== targetName);
            if (itemsToProcess.length === 0) {
                 return this.logStatus(`ℹ️ No other items found to combine with "${targetName}".`);
            }

            this.isRunning = true;
            this.logStatus(`🚀 Starting combinations with "${targetName}"...`);

            let processedCount = 0;
            const totalItems = itemsToProcess.length;

            // Get initial target element reference (might change if combined upon)
            let currentTargetElement = targetElement;

            for (const itemName of itemsToProcess) {
                if (!this.isRunning) break; // Check stop flag before processing item

                const sourceElement = this.getElement(itemName);

                // Re-acquire target element before *each* combination, as it might have been replaced
                currentTargetElement = this.getElement(targetName);
                if (!currentTargetElement || !document.body.contains(currentTargetElement)) {
                    this.logStatus(`⛔ Target "${targetName}" disappeared! Stopping.`);
                    this.isRunning = false;
                    break;
                 }

                // Validate source element
                if (!sourceElement || !document.body.contains(sourceElement)) {
                     this.logStatus(`⚠️ Skipping "${itemName}": Element not found/removed.`, true);
                    continue; // Skip if source element disappeared
                }

                 processedCount++;
                 this.logStatus(`🚀 Combining ${targetName} (${processedCount}/${totalItems}) + ${itemName}`, true); // Progress update

                try {
                    // Simulate drag A onto B (source onto target)
                    await this.simulateCombo(sourceElement, currentTargetElement);

                    if (!this.isRunning) break; // Check stop flag immediately after combo attempt

                    // Wait after combination attempt
                    await new Promise(res => setTimeout(res, CONFIG.comboLoopDelay));

                    if (!this.isRunning) break; // Check stop flag after delay

                     // --- Crucial: Rescan after a combo attempt ---
                     // A combo might create a new element or remove old ones.
                     // We need the updated item list for the next iteration.
                     // Use a small delay before scanning to let the DOM update.
                    await new Promise(res => setTimeout(res, 50)); // Short delay for DOM updates
                    if (!this.isRunning) break;
                    this.scanItems();
                    // The target element reference might be invalid now, it will be re-fetched at the start of the next loop.

                } catch (error) {
                    this.logStatus(`❌ Error combining ${itemName} with ${targetName}: ${error.message}`);
                    console.error(`[AutoCombo] Error during combo for item "${itemName}":`, error);
                    // Decide whether to stop or continue on error
                     this.stop(); // Stop on error for safety
                     break;
                }
            }

            if (this.isRunning) {
                this.logStatus(`✅ Combo pass complete for "${targetName}". Processed ${processedCount} items.`);
                this.isRunning = false;
            } else {
                this.logStatus(`⛔ Combo process stopped.`);
            }
        }

        // --- Simulation ---

        async simulateCombo(aEl, bEl) {
            if (!this.isRunning || !this.manualBaseCoords) return; // Early exit

            const dropX = this.manualBaseCoords.x;
            const dropY = this.manualBaseCoords.y;

             // Make sure elements are valid before trying to drag
             if (!aEl || !document.body.contains(aEl)) {
                 throw new Error(`Source element "${aEl?.textContent?.trim() || 'unknown'}" is invalid.`);
             }
             if (!bEl || !document.body.contains(bEl)) {
                 throw new Error(`Target element "${bEl?.textContent?.trim() || 'unknown'}" is invalid.`);
             }


            // Drag Source Element (A) from sidebar to drop point
            await this.simulateDrag(aEl, dropX, dropY, 'blue');
             if (!this.isRunning) return; // Check stop flag

            // Short pause after dropping first element
            await new Promise(r => setTimeout(r, CONFIG.postDragDelay));
             if (!this.isRunning) return; // Check stop flag

            // Drag Target Element (B) from sidebar to the *same* drop point
            // Re-check validity as it might have changed if A was B
            if (!bEl || !document.body.contains(bEl)) {
                 // If B disappeared (maybe it was combined with A already), we might need to skip
                 // or handle this. For now, throw error if it was expected.
                  if (aEl !== bEl) { // Only error if B wasn't the same as A
                     throw new Error(`Target element "${bEl?.textContent?.trim() || 'unknown'}" became invalid after dragging source.`);
                  } else {
                      console.warn("[AutoCombo] Target element was same as source and may have been consumed.");
                      return; // Don't try to drag the second item if it was the same and gone
                  }
            }
            await this.simulateDrag(bEl, dropX, dropY, 'green');
        }

        async simulateDrag(element, dropX, dropY, markerColor) {
            if (!this.isRunning || !element || !document.body.contains(element)) return;

            const rect = element.getBoundingClientRect();
             // Calculate center coordinates relative to the viewport (clientX/Y)
            const clientStartX = rect.left + rect.width / 2;
            const clientStartY = rect.top + rect.height / 2;
            // Drop coordinates are already absolute, convert to clientX/Y
            const clientDropX = dropX - window.scrollX;
            const clientDropY = dropY - window.scrollY;


            this.showDebugMarker(clientStartX + window.scrollX, clientStartY + window.scrollY, markerColor); // Show marker at absolute start pos
            this.showDebugMarker(dropX, dropY, markerColor); // Show marker at absolute drop pos

            try {
                // Mousedown on the element's center
                element.dispatchEvent(new PointerEvent('pointerdown', {
                    bubbles: true, cancelable: true, view: window, button: 0, // Main button
                    clientX: clientStartX, clientY: clientStartY, pointerId: 1, isPrimary: true
                }));
                await new Promise(res => setTimeout(res, CONFIG.dragStepDelay));
                if (!this.isRunning) return;

                // Mousemove to the drop position (dispatch on document/window)
                window.dispatchEvent(new PointerEvent('pointermove', { // Use window for broader capture? Or stick to document? Window seems safer.
                    bubbles: true, cancelable: true, view: window,
                    clientX: clientDropX, clientY: clientDropY, pointerId: 1, isPrimary: true
                    // `buttons: 1` is implicit in pointermove after pointerdown in many UIs, but maybe not needed here.
                }));
                await new Promise(res => setTimeout(res, CONFIG.dragStepDelay));
                 if (!this.isRunning) return;

                // Mouseup at the drop position (dispatch on document/window)
                window.dispatchEvent(new PointerEvent('pointerup', {
                    bubbles: true, cancelable: true, view: window, button: 0,
                    clientX: clientDropX, clientY: clientDropY, pointerId: 1, isPrimary: true
                }));

            } catch (error) {
                 console.error('[AutoCombo] Error during drag simulation:', error);
                 this.logStatus(`❌ Drag simulation error: ${error.message}`);
                 throw error; // Re-throw to be caught by startAutoCombo loop
            }
        }

        // --- UI & Suggestions ---

        _updateSuggestions() {
             // Update suggestion box position relative to input, in case window resized/scrolled
             const inputRect = this.targetInput.getBoundingClientRect();
             const panelRect = this.panel.getBoundingClientRect();
             // Use fixed positioning coordinates if panel is fixed, else absolute
             const panelTop = parseFloat(window.getComputedStyle(this.panel).top);
             const panelLeft = parseFloat(window.getComputedStyle(this.panel).left);
             this.suggestionBox.style.top = `${inputRect.bottom - panelTop}px`; // Position below input, relative to panel top
             this.suggestionBox.style.left = `${inputRect.left - panelLeft}px`; // Align left edge relative to panel left
             this.suggestionBox.style.width = `${inputRect.width}px`;


            const query = this.targetInput.value.toLowerCase();
            if (!query) {
                this.suggestions = [];
                this.suggestionBox.style.display = 'none';
                return;
            }

            // Filter based on current item map
            this.suggestions = [...this.itemElementMap.keys()]
                .filter(name => name.toLowerCase().includes(query))
                .sort((a, b) => a.toLowerCase().indexOf(query) - b.toLowerCase().indexOf(query) || a.localeCompare(b)) // Sort by relevance then alpha
                .slice(0, CONFIG.suggestionLimit);

            this.suggestionIndex = -1; // Reset selection
            this._updateSuggestionUI();
        }

        _updateSuggestionUI() {
             this.suggestionBox.innerHTML = ''; // Clear previous suggestions
             this.suggestionBox.style.display = this.suggestions.length ? 'block' : 'none';

             if (!this.suggestions.length) return;

            // Position already updated in _updateSuggestions

            this.suggestions.forEach((name, index) => {
                const div = document.createElement('div');
                div.textContent = name;
                div.className = CONFIG.suggestionItemClass; // Use class for styling
                div.addEventListener('mousedown', (e) => { // Use mousedown to potentially fire before blur
                    e.preventDefault(); // Prevent input blur before click registers
                    this._handleSuggestionSelection(name);
                });
                if (index === this.suggestionIndex) {
                    div.classList.add('highlighted'); // Add highlight class
                }
                this.suggestionBox.appendChild(div);
            });

             // Ensure highlighted item is visible
             this._scrollSuggestionIntoView();
        }

        _handleSuggestionKey(e) {
            if (this.suggestionBox.style.display !== 'block' || !this.suggestions.length) {
                 // If suggestions aren't visible but user presses Enter, try starting combo directly
                 if (e.key === CONFIG.keyEnter) {
                     e.preventDefault();
                      this.suggestionBox.style.display = 'none'; // Hide just in case
                     this.startAutoCombo();
                 }
                 return;
             }

            const numSuggestions = this.suggestions.length;

            switch (e.key) {
                case CONFIG.keyArrowDown:
                case CONFIG.keyTab: // Allow Tab for navigation
                    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.suggestionIndex < numSuggestions) {
                        this._handleSuggestionSelection(this.suggestions[this.suggestionIndex]);
                    } else {
                        // If no suggestion selected, use current input value and start
                        this.suggestionBox.style.display = 'none';
                        this.startAutoCombo();
                    }
                    break;
                default:
                     // Any other key - let the input handle it, suggestions will update via 'input' event
                    break;
            }
        }

        _updateSuggestionHighlight() {
            const children = Array.from(this.suggestionBox.children);
            children.forEach((child, i) => {
                child.classList.toggle('highlighted', i === this.suggestionIndex);
            });
            this._scrollSuggestionIntoView();
        }

         _scrollSuggestionIntoView() {
             const highlightedItem = this.suggestionBox.querySelector(`.${CONFIG.suggestionItemClass}.highlighted`);
             if (highlightedItem) {
                 // Use setTimeout to allow DOM to update highlight class before scrolling
                 setTimeout(() => {
                     highlightedItem.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
                 }, CONFIG.suggestionHighlightDelay);
             }
         }

        _handleSuggestionSelection(name) {
            this.targetInput.value = name; // Set input value
            this.suggestionBox.style.display = 'none'; // Hide suggestions
            this.suggestions = []; // Clear suggestions array
            this.targetInput.focus(); // Keep focus on input often desired
            // Do not automatically start combo on selection, let user press Start
        }


        // --- Event Handlers ---

        _handleCanvasClick(e) {
            if (!this.awaitingClick) return;

             // Check if click is outside the panel and sidebar to avoid setting position there
             const sidebar = document.querySelector(CONFIG.gameContainerSelector);
             if (this.panel.contains(e.target) || (sidebar && sidebar.contains(e.target))) {
                 // Click was inside UI panel or sidebar, ignore for setting drop position
                 this.logStatus('🖱️ Please click on the main empty game area.');
                 return;
             }

             // Click is valid for setting position
             e.preventDefault();
             e.stopPropagation(); // Stop propagation to prevent other listeners

            // Restore default cursor
            document.body.style.cursor = 'default';

            // Use clientX/clientY + scroll offset for absolute positioning
            const clickX = e.clientX + window.scrollX;
            const clickY = e.clientY + window.scrollY;

            this.manualBaseCoords = { x: clickX, y: clickY };
            this.awaitingClick = false;
            this.baseReady = true;
            this._saveDropPosition(); // Save the new position
            this.showDebugMarker(this.manualBaseCoords.x, this.manualBaseCoords.y, 'red');
            this.logStatus(`📍 Drop point set at (${Math.round(clickX)}, ${Math.round(clickY)}). Ready.`);
        }

        _saveDropPosition() {
            if (this.manualBaseCoords) {
                try {
                    localStorage.setItem(CONFIG.storageKeyCoords, JSON.stringify(this.manualBaseCoords));
                } catch (e) {
                    console.error('[AutoCombo] Error saving coordinates:', e);
                    this.logStatus('❌ Error saving drop position.');
                }
            }
        }

        // --- Utilities ---

        getElement(name) {
             // Infinite Craft items can have emojis/spans, match text content carefully
             const element = this.itemElementMap.get(name);
             // Add extra check: Make sure the element text still matches, in case map is stale
             if (element && element.textContent?.trim() === name && document.body.contains(element)) {
                return element;
             }
             // If not found or text doesn't match, return null
             if (element && element.textContent?.trim() !== name) {
                console.warn(`[AutoCombo] Stale element reference for "${name}". Map text: "${element.textContent?.trim()}"`);
                this.itemElementMap.delete(name); // Remove stale entry
             }
            return null; // Explicitly return null if not found or invalid
        }

        showDebugMarker(x, y, color = 'red', duration = CONFIG.debugMarkerDuration) {
            const dot = document.createElement('div');
            dot.className = CONFIG.debugMarkerClass; // Use class
            Object.assign(dot.style, {
                top: `${y - 4}px`, // Center the dot
                left: `${x - 4}px`,
                backgroundColor: color,
            });
            document.body.appendChild(dot);
            setTimeout(() => dot.remove(), duration);
        }

        logStatus(msg, isProgressUpdate = false) {
             if (this.statusBox) {
                this.statusBox.textContent = msg;
             }
             // Avoid flooding console with rapid progress updates if desired
             // Log initial/final/error messages, and progress only if running or explicitly not progress
            if (!isProgressUpdate || this.isRunning || msg.startsWith('✅') || msg.startsWith('⛔') || msg.startsWith('❌') || msg.startsWith('⚠️') || msg.startsWith('ℹ️') || msg.startsWith('📍') || msg.startsWith('🖱️') ) {
                 console.log('[AutoCombo]', msg);
            }
        }
    }

    // --- Initialize the script ---
    // No need for DOMContentLoaded check due to @run-at document-end
    console.log("[AutoCombo] Initializing script...");
    new AutoTargetCombo();

})();