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