您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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();
- }
- })();