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