Infinite Craft - Auto Combiner V2 (with Fail Memory)

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

  1. // ==UserScript==
  2. // @name Infinite Craft - Auto Combiner V2 (with Fail Memory)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.1
  5. // @description Automates combining a target element with all others in Infinite Craft, remembering failed combinations to speed up runs.
  6. // @author YourName (or Generated)
  7. // @match https://neal.fun/infinite-craft/
  8. // @grant none
  9. // @run-at document-idle
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (() => {
  14. // --- CONFIGURATION --- (Adjust delays if needed)
  15. const CONFIG = {
  16. // Selectors specific to Infinite Craft (Verify with DevTools if game updates)
  17. itemSelector: '.item', // Selector for draggable items
  18. // itemTextAttribute: NO LONGER USED - uses textContent now
  19. gameContainerSelector: '.container.main-container', // More specific container for observer
  20.  
  21. // UI Element IDs & Classes
  22. panelId: 'auto-combo-panel',
  23. targetInputId: 'auto-combo-target-input',
  24. suggestionBoxId: 'auto-combo-suggestion-box',
  25. suggestionItemClass: 'auto-combo-suggestion-item',
  26. statusBoxId: 'auto-combo-status',
  27. startButtonId: 'auto-combo-start-button',
  28. stopButtonId: 'auto-combo-stop-button',
  29. // setPositionButtonId: REMOVED
  30. clearFailedButtonId: 'auto-combo-clear-failed-button',
  31. debugMarkerClass: 'auto-combo-debug-marker', // Still useful for visualizing drag path
  32.  
  33. // Delays (ms) - Tune these based on game responsiveness
  34. interComboDelay: 100, // Delay between trying different combinations
  35. postComboScanDelay: 650, // Delay AFTER a combo attempt BEFORE checking result
  36. dragStepDelay: 15, // Delay between mouse events during a single drag
  37. // postDragDelay: REMOVED (only one drag per combo now)
  38. scanDebounceDelay: 300, // Delay before rescanning items after DOM changes
  39. suggestionHighlightDelay: 50,
  40.  
  41. // Behavior
  42. suggestionLimit: 20,
  43. debugMarkerDuration: 1000, // Shorter duration might be less intrusive
  44.  
  45. // Keys
  46. keyArrowUp: 'ArrowUp',
  47. keyArrowDown: 'ArrowDown',
  48. keyEnter: 'Enter',
  49. keyTab: 'Tab',
  50.  
  51. // Storage (V2 to avoid conflicts if you used older versions)
  52. // storageKeyCoords: REMOVED
  53. storageKeyFailedCombos: 'infCraftAutoComboFailedCombosV2',
  54.  
  55. // Styling & Z-Index
  56. panelZIndex: 10010, // Slightly higher Z-index just in case
  57. suggestionZIndex: 10011,
  58. markerZIndex: 10012,
  59. };
  60.  
  61. // --- CORE CLASS ---
  62. class AutoTargetCombo {
  63. constructor() {
  64. console.log('[AutoCombo] Initializing for Infinite Craft...');
  65. this.itemElementMap = new Map(); // Map<string, Element>
  66. // this.manualBaseCoords = null; // REMOVED
  67. // this.awaitingClick = false; // REMOVED
  68. // this.baseReady = false; // REMOVED (no separate drop point needed)
  69. this.isRunning = false;
  70. this.suggestionIndex = -1;
  71. this.suggestions = [];
  72. this.scanDebounceTimer = null;
  73. this.failedCombos = new Set();
  74.  
  75. // UI References
  76. this.panel = null;
  77. this.targetInput = null;
  78. this.suggestionBox = null;
  79. this.statusBox = null;
  80. this.startButton = null;
  81. this.stopButton = null;
  82. // this.setPositionButton = null; // REMOVED
  83. this.clearFailedButton = null;
  84.  
  85. // --- Initialization Steps ---
  86. try {
  87. this._injectStyles();
  88. this._setupUI();
  89.  
  90. // Check if essential UI elements were found after setup
  91. if (!this.panel || !this.targetInput || !this.statusBox || !this.startButton || !this.stopButton || !this.clearFailedButton) {
  92. throw new Error("One or more critical UI elements missing after setup. Aborting.");
  93. }
  94.  
  95. this._setupEventListeners();
  96. this._loadFailedCombos(); // Load saved failures
  97. this.observeDOM();
  98. this.scanItems(); // Perform initial scan
  99. this.logStatus('Ready.');
  100. console.log('[AutoCombo] Initialization complete.');
  101. } catch (error) {
  102. console.error('[AutoCombo] CRITICAL ERROR during initialization:', error);
  103. this.logStatus(`❌ INIT FAILED: ${error.message}`, 'status-error');
  104. // Clean up partial UI if needed
  105. if (this.panel && this.panel.parentNode) {
  106. this.panel.parentNode.removeChild(this.panel);
  107. }
  108. }
  109. }
  110.  
  111. // --- Initialization & Setup ---
  112.  
  113. _injectStyles() {
  114. if (document.getElementById(`${CONFIG.panelId}-styles`)) return;
  115. const css = `
  116. #${CONFIG.panelId} {
  117. position: fixed; top: 15px; left: 15px; z-index: ${CONFIG.panelZIndex};
  118. background: rgba(250, 250, 250, 0.97); padding: 12px; border: 1px solid #aaa; border-radius: 8px;
  119. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 14px; width: 260px; /* Slightly narrower */ color: #111;
  120. box-shadow: 0 5px 15px rgba(0,0,0,0.25); display: flex; flex-direction: column; gap: 6px; /* Increased gap slightly */
  121. }
  122. #${CONFIG.panelId} * { box-sizing: border-box; }
  123. #${CONFIG.panelId} div:first-child { /* Title */
  124. font-weight: bold; margin-bottom: 4px; text-align: center; color: #333; font-size: 15px; padding-bottom: 4px; border-bottom: 1px solid #ddd;
  125. }
  126. #${CONFIG.panelId} input, #${CONFIG.panelId} button {
  127. width: 100%; padding: 9px 10px; font-size: 14px;
  128. border: 1px solid #ccc; border-radius: 4px;
  129. }
  130. #${CONFIG.panelId} input { background-color: #fff; color: #000; }
  131. #${CONFIG.panelId} button {
  132. cursor: pointer; background-color: #f0f0f0; color: #333; transition: background-color 0.2s ease, transform 0.1s ease;
  133. border: 1px solid #bbb; text-align: center;
  134. }
  135. #${CONFIG.panelId} button:hover { background-color: #e0e0e0; }
  136. #${CONFIG.panelId} button:active { transform: scale(0.98); }
  137.  
  138. /* Specific Button Styles */
  139. #${CONFIG.panelId} #${CONFIG.startButtonId} { background-color: #4CAF50; color: white; border-color: #3a8d3d;}
  140. #${CONFIG.panelId} #${CONFIG.startButtonId}:hover { background-color: #45a049; }
  141. #${CONFIG.panelId} #${CONFIG.stopButtonId} { background-color: #f44336; color: white; border-color: #c4302b;}
  142. #${CONFIG.panelId} #${CONFIG.stopButtonId}:hover { background-color: #da190b; }
  143. /* setPositionButton styles removed */
  144. #${CONFIG.panelId} #${CONFIG.clearFailedButtonId} { background-color: #ff9800; color: white; border-color: #c67600;}
  145. #${CONFIG.panelId} #${CONFIG.clearFailedButtonId}:hover { background-color: #f57c00; }
  146.  
  147. #${CONFIG.suggestionBoxId} {
  148. display: none; border: 1px solid #aaa; background: #fff;
  149. position: absolute; max-height: 160px; overflow-y: auto;
  150. z-index: ${CONFIG.suggestionZIndex}; box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  151. border-radius: 0 0 4px 4px; margin-top: -1px; /* Align with input */ font-size: 13px;
  152. }
  153. .${CONFIG.suggestionItemClass} {
  154. padding: 7px 12px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #222;
  155. }
  156. .${CONFIG.suggestionItemClass}:hover { background-color: #f0f0f0; }
  157. .${CONFIG.suggestionItemClass}.highlighted { background-color: #007bff; color: white; }
  158.  
  159. #${CONFIG.statusBoxId} {
  160. margin-top: 6px; color: #333; font-weight: 500; font-size: 13px; text-align: center;
  161. padding: 7px; background-color: #f9f9f9; border-radius: 3px; border: 1px solid #e5e5e5;
  162. }
  163. /* Status styles (unchanged) */
  164. #${CONFIG.statusBoxId}.status-running { color: #007bff; }
  165. #${CONFIG.statusBoxId}.status-stopped { color: #dc3545; }
  166. #${CONFIG.statusBoxId}.status-success { color: #28a745; }
  167. #${CONFIG.statusBoxId}.status-warning { color: #ffc107; text-shadow: 0 0 1px #aaa; }
  168. #${CONFIG.statusBoxId}.status-error { color: #dc3545; font-weight: bold; }
  169.  
  170. .${CONFIG.debugMarkerClass} {
  171. position: absolute; width: 10px; height: 10px; border-radius: 50%;
  172. z-index: ${CONFIG.markerZIndex}; pointer-events: none; opacity: 0.80;
  173. box-shadow: 0 0 5px 2px rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.5);
  174. transition: opacity 0.5s ease-out; /* Fade out */
  175. }
  176. `;
  177. const styleSheet = document.createElement("style");
  178. styleSheet.id = `${CONFIG.panelId}-styles`;
  179. styleSheet.type = "text/css";
  180. styleSheet.innerText = css;
  181. document.head.appendChild(styleSheet);
  182. }
  183.  
  184. _setupUI() {
  185. const existingPanel = document.getElementById(CONFIG.panelId);
  186. if (existingPanel) existingPanel.remove();
  187.  
  188. this.panel = document.createElement('div');
  189. this.panel.id = CONFIG.panelId;
  190. // Removed Set Drop Position button from HTML
  191. this.panel.innerHTML = `
  192. <div>✨ Infinite Auto Combiner ✨</div>
  193. <input id="${CONFIG.targetInputId}" placeholder="Target Element (e.g. Water)" autocomplete="off">
  194. <div id="${CONFIG.suggestionBoxId}"></div>
  195. <button id="${CONFIG.startButtonId}">▶️ Combine with All Others</button>
  196. <button id="${CONFIG.clearFailedButtonId}">🗑️ Clear Failed Memory</button>
  197. <button id="${CONFIG.stopButtonId}">⛔ Stop</button>
  198. <div id="${CONFIG.statusBoxId}">Initializing...</div>
  199. `;
  200. document.body.appendChild(this.panel);
  201.  
  202. // Get references using panel.querySelector
  203. this.targetInput = this.panel.querySelector(`#${CONFIG.targetInputId}`);
  204. this.suggestionBox = this.panel.querySelector(`#${CONFIG.suggestionBoxId}`);
  205. this.statusBox = this.panel.querySelector(`#${CONFIG.statusBoxId}`);
  206. this.startButton = this.panel.querySelector(`#${CONFIG.startButtonId}`);
  207. this.stopButton = this.panel.querySelector(`#${CONFIG.stopButtonId}`);
  208. // this.setPositionButton = null; // REMOVED
  209. this.clearFailedButton = this.panel.querySelector(`#${CONFIG.clearFailedButtonId}`);
  210.  
  211. // Log check (removed position button check)
  212. console.log('[AutoCombo] UI Element References:', {
  213. panel: !!this.panel, targetInput: !!this.targetInput, suggestionBox: !!this.suggestionBox,
  214. statusBox: !!this.statusBox, startButton: !!this.startButton, stopButton: !!this.stopButton,
  215. clearFailedButton: !!this.clearFailedButton
  216. });
  217. // Crucial Check (removed position button check)
  218. if (!this.targetInput || !this.statusBox || !this.startButton || !this.stopButton || !this.clearFailedButton) {
  219. throw new Error("One or more required UI elements not found within the panel.");
  220. }
  221. }
  222.  
  223. _setupEventListeners() {
  224. if (!this.targetInput || !this.startButton || !this.stopButton || !this.clearFailedButton) {
  225. throw new Error("Cannot setup listeners: Required UI elements missing.");
  226. }
  227.  
  228. this.targetInput.addEventListener('input', () => this._updateSuggestions());
  229. this.targetInput.addEventListener('keydown', e => this._handleSuggestionKey(e));
  230. this.targetInput.addEventListener('focus', () => this._updateSuggestions());
  231.  
  232. document.addEventListener('click', (e) => {
  233. if (this.panel && !this.panel.contains(e.target) && this.suggestionBox && !this.suggestionBox.contains(e.target)) {
  234. if (this.suggestionBox.style.display === 'block') this.suggestionBox.style.display = 'none';
  235. }
  236. }, true);
  237.  
  238. this.startButton.onclick = () => this.startAutoCombo();
  239. this.stopButton.onclick = () => this.stop();
  240. // setPositionButton listener REMOVED
  241. this.clearFailedButton.onclick = () => this._clearFailedCombos();
  242.  
  243. // Canvas click listener REMOVED (no longer needed)
  244. }
  245.  
  246. _loadFailedCombos() {
  247. const savedCombos = localStorage.getItem(CONFIG.storageKeyFailedCombos);
  248. let loadedCount = 0;
  249. if (savedCombos) {
  250. try {
  251. const parsedCombos = JSON.parse(savedCombos);
  252. if (Array.isArray(parsedCombos)) {
  253. const validCombos = parsedCombos.filter(item => typeof item === 'string');
  254. this.failedCombos = new Set(validCombos);
  255. loadedCount = this.failedCombos.size;
  256. if (loadedCount > 0) {
  257. this.logStatus(`📚 Loaded ${loadedCount} failed combos.`, 'status-success');
  258. }
  259. } else {
  260. localStorage.removeItem(CONFIG.storageKeyFailedCombos);
  261. this.failedCombos = new Set();
  262. }
  263. } catch (e) {
  264. console.error('[AutoCombo] Error parsing failed combos:', e);
  265. localStorage.removeItem(CONFIG.storageKeyFailedCombos);
  266. this.failedCombos = new Set();
  267. }
  268. } else {
  269. this.failedCombos = new Set();
  270. }
  271. console.log(`[AutoCombo] Failed combos loaded: ${loadedCount}`);
  272. }
  273.  
  274. _saveFailedCombos() {
  275. if (this.failedCombos.size === 0) {
  276. localStorage.removeItem(CONFIG.storageKeyFailedCombos);
  277. return;
  278. }
  279. try {
  280. localStorage.setItem(CONFIG.storageKeyFailedCombos, JSON.stringify(Array.from(this.failedCombos)));
  281. } catch (e) {
  282. console.error('[AutoCombo] Error saving failed combos:', e);
  283. this.logStatus('❌ Error saving fails!', 'status-error');
  284. }
  285. }
  286.  
  287. _clearFailedCombos() {
  288. const count = this.failedCombos.size;
  289. this.failedCombos.clear();
  290. localStorage.removeItem(CONFIG.storageKeyFailedCombos);
  291. this.logStatus(`🗑️ Cleared ${count} failed combos.`, 'status-success');
  292. console.log(`[AutoCombo] Cleared ${count} failed combos.`);
  293. }
  294.  
  295. // --- Core Logic ---
  296.  
  297. scanItems() {
  298. clearTimeout(this.scanDebounceTimer);
  299. this.scanDebounceTimer = null;
  300.  
  301. const items = document.querySelectorAll(CONFIG.itemSelector);
  302. let changed = false;
  303. const currentNames = new Set();
  304. const oldSize = this.itemElementMap.size;
  305.  
  306. for (const el of items) {
  307. if (!el || typeof el.textContent !== 'string') continue;
  308. // *** Use textContent for Infinite Craft ***
  309. const name = el.textContent.trim();
  310. if (name) { // Ensure name is not empty after trimming
  311. currentNames.add(name);
  312. if (!this.itemElementMap.has(name) || this.itemElementMap.get(name) !== el) {
  313. this.itemElementMap.set(name, el);
  314. changed = true;
  315. }
  316. }
  317. }
  318.  
  319. const currentKeys = Array.from(this.itemElementMap.keys());
  320. for (const name of currentKeys) {
  321. if (!currentNames.has(name)) {
  322. this.itemElementMap.delete(name);
  323. changed = true;
  324. }
  325. }
  326.  
  327. if (changed && !this.isRunning) {
  328. const newSize = this.itemElementMap.size;
  329. const diff = newSize - oldSize;
  330. let logMsg = `🔍 Scan: ${newSize} items`;
  331. if (diff > 0) logMsg += ` (+${diff})`; else if (diff < 0) logMsg += ` (${diff})`;
  332. this.logStatus(logMsg);
  333. console.log(`[AutoCombo] ${logMsg}`);
  334. // Update suggestions if input is focused
  335. if (document.activeElement === this.targetInput) {
  336. this._updateSuggestions();
  337. }
  338. }
  339. return changed;
  340. }
  341.  
  342. observeDOM() {
  343. // Use the configured game container selector
  344. const targetNode = document.querySelector(CONFIG.gameContainerSelector);
  345. if (!targetNode) {
  346. console.error("[AutoCombo] Cannot observe DOM: Target node not found:", CONFIG.gameContainerSelector);
  347. this.logStatus(`❌ Error: Observer target (${CONFIG.gameContainerSelector}) not found!`, "status-error");
  348. // Fallback to body? Maybe not ideal if body is too broad.
  349. // targetNode = document.body;
  350. return;
  351. }
  352.  
  353. const observer = new MutationObserver((mutationsList) => {
  354. let potentiallyRelevantChange = false;
  355. for (const mutation of mutationsList) {
  356. if (mutation.type === 'childList') {
  357. const checkNodes = (nodes) => {
  358. if (!nodes) return false;
  359. for(const node of nodes) {
  360. if (node.nodeType === Node.ELEMENT_NODE && node.matches && node.matches(CONFIG.itemSelector)) return true;
  361. // Maybe check if node *contains* an item selector too? More expensive.
  362. // if (node.nodeType === Node.ELEMENT_NODE && node.querySelector && node.querySelector(CONFIG.itemSelector)) return true;
  363. }
  364. return false;
  365. }
  366. if (checkNodes(mutation.addedNodes) || checkNodes(mutation.removedNodes)) {
  367. potentiallyRelevantChange = true;
  368. break;
  369. }
  370. }
  371. // No attribute watching needed for textContent changes
  372. }
  373.  
  374. if (potentiallyRelevantChange) {
  375. clearTimeout(this.scanDebounceTimer);
  376. this.scanDebounceTimer = setTimeout(() => {
  377. // console.log("[AutoCombo] DOM change detected, rescanning items..."); // Can be noisy
  378. this.scanItems();
  379. }, CONFIG.scanDebounceDelay);
  380. }
  381. });
  382.  
  383. observer.observe(targetNode, {
  384. childList: true,
  385. subtree: true, // Need subtree as items are nested
  386. // attributes: false // Not watching attributes anymore
  387. });
  388. console.log("[AutoCombo] DOM Observer started on:", targetNode);
  389. }
  390.  
  391. stop() {
  392. if (!this.isRunning) return;
  393. this.isRunning = false;
  394. clearTimeout(this.scanDebounceTimer);
  395. this.logStatus('⛔ Combo process stopped.', 'status-stopped');
  396. console.log('[AutoCombo] Stop requested.');
  397. }
  398.  
  399. async startAutoCombo() {
  400. if (this.isRunning) {
  401. this.logStatus('⚠️ Already running.', 'status-warning'); return;
  402. }
  403. // No baseReady check needed
  404.  
  405. const targetName = this.targetInput.value.trim();
  406. if (!targetName) {
  407. this.logStatus('⚠️ Enter Target Element', 'status-warning'); this.targetInput.focus(); return;
  408. }
  409.  
  410. this.scanItems(); // Ensure map is fresh
  411. let targetElement = this.getElement(targetName);
  412. if (!targetElement || !document.body.contains(targetElement)) {
  413. this.logStatus(`⚠️ Target "${targetName}" not found.`, 'status-warning'); this.targetInput.focus(); return;
  414. }
  415.  
  416. const itemsToProcess = Array.from(this.itemElementMap.keys()).filter(name => name !== targetName);
  417. if (itemsToProcess.length === 0) {
  418. this.logStatus(`ℹ️ No other items found to combine with "${targetName}".`); return;
  419. }
  420.  
  421. this.isRunning = true;
  422. this.logStatus(`🚀 Starting... Target: ${targetName} (${itemsToProcess.length} others)`, 'status-running');
  423. console.log(`[AutoCombo] Starting combinations for "${targetName}". Items: ${itemsToProcess.length}. Fails: ${this.failedCombos.size}`);
  424.  
  425. let processedCount = 0, attemptedCount = 0, successCount = 0, skippedCount = 0;
  426. const totalPotentialCombos = itemsToProcess.length;
  427.  
  428. for (const itemName of itemsToProcess) {
  429. if (!this.isRunning) break;
  430.  
  431. processedCount++;
  432. const progress = `(${processedCount}/${totalPotentialCombos})`;
  433.  
  434. const comboKey = this._getComboKey(targetName, itemName);
  435. if (this.failedCombos.has(comboKey)) {
  436. if (processedCount % 20 === 0 || processedCount === totalPotentialCombos) {
  437. this.logStatus(`⏭️ Skipping known fails... ${progress}`, 'status-running');
  438. }
  439. console.log(`[AutoCombo] ${progress} Skipping known fail: ${targetName} + ${itemName}`);
  440. skippedCount++;
  441. await new Promise(res => setTimeout(res, 2)); // Minimal delay
  442. continue;
  443. }
  444.  
  445. // Re-get target each time, it might have been recreated
  446. targetElement = this.getElement(targetName);
  447. if (!targetElement || !document.body.contains(targetElement)) {
  448. this.logStatus(`⛔ Target "${targetName}" lost! Stopping.`, 'status-error');
  449. console.error(`[AutoCombo] Target element "${targetName}" disappeared mid-process.`);
  450. this.isRunning = false; break;
  451. }
  452.  
  453. const sourceElement = this.getElement(itemName);
  454. if (!sourceElement || !document.body.contains(sourceElement)) {
  455. this.logStatus(`⚠️ Skipping "${itemName}" ${progress}: Elem lost.`, 'status-warning');
  456. console.warn(`[AutoCombo] ${progress} Skipping "${itemName}": Element not found/removed.`);
  457. continue;
  458. }
  459.  
  460. this.logStatus(`⏳ Trying: ${targetName} + ${itemName} ${progress}`, 'status-running');
  461. console.log(`[AutoCombo] ${progress} Attempting: ${targetName} + ${itemName}`);
  462. attemptedCount++;
  463.  
  464. const itemsBeforeCombo = new Set(this.itemElementMap.keys());
  465.  
  466. try {
  467. // *** Simulate Drag Source Onto Target ***
  468. await this.simulateCombo(sourceElement, targetElement);
  469. if (!this.isRunning) break;
  470.  
  471. await new Promise(res => setTimeout(res, CONFIG.postComboScanDelay));
  472. if (!this.isRunning) break;
  473.  
  474. // *** Check Result ***
  475. // console.log("[AutoCombo] Explicitly rescanning after combo delay..."); // Debug
  476. this.scanItems(); // Force scan to update map
  477. const itemsAfterCombo = new Set(this.itemElementMap.keys());
  478.  
  479. let newItemFound = null;
  480. for (const itemAfter of itemsAfterCombo) {
  481. if (!itemsBeforeCombo.has(itemAfter)) { newItemFound = itemAfter; break; }
  482. }
  483.  
  484. if (newItemFound) {
  485. successCount++;
  486. this.logStatus(`✨ NEW: ${newItemFound}! (${targetName} + ${itemName})`, 'status-success');
  487. console.log(`[AutoCombo] SUCCESS! New: ${newItemFound} from ${targetName} + ${itemName}`);
  488. // No need to check if target was consumed, as scanItems will update map anyway
  489. // and we re-get targetElement at the start of the loop.
  490. } else {
  491. const targetStillExists = itemsAfterCombo.has(targetName);
  492. const sourceStillExists = itemsAfterCombo.has(itemName);
  493. this.logStatus(`❌ Failed: ${targetName} + ${itemName} (T:${targetStillExists}, S:${sourceStillExists})`, 'status-running');
  494. console.log(`[AutoCombo] FAILURE: No new item from ${targetName} + ${itemName}. T:${targetStillExists}, S:${sourceStillExists}`);
  495. this.failedCombos.add(comboKey);
  496. this._saveFailedCombos();
  497. }
  498.  
  499. await new Promise(res => setTimeout(res, CONFIG.interComboDelay));
  500.  
  501. } catch (error) {
  502. this.logStatus(`❌ Error combining ${itemName}: ${error.message}`, 'status-error');
  503. console.error(`[AutoCombo] Error during combo for "${itemName}" + "${targetName}":`, error);
  504. // Decide if stop is needed
  505. // this.stop(); break;
  506. }
  507. } // End loop
  508.  
  509. if (this.isRunning) {
  510. this.isRunning = false;
  511. const summary = `✅ Done. Tried: ${attemptedCount}, New: ${successCount}, Skipped: ${skippedCount}.`;
  512. this.logStatus(summary, 'status-success');
  513. console.log(`[AutoCombo] ${summary}`);
  514. } else {
  515. const summary = `⛔ Stopped. Tried: ${attemptedCount}, New: ${successCount}, Skipped: ${skippedCount}.`;
  516. console.log(`[AutoCombo] ${summary}`);
  517. }
  518. }
  519.  
  520.  
  521. // --- Simulation (Modified for Infinite Craft) ---
  522.  
  523. async simulateCombo(sourceElement, targetElement) {
  524. if (!this.isRunning) return;
  525.  
  526. // *** Get Target Position ***
  527. const targetRect = targetElement.getBoundingClientRect();
  528. if (targetRect.width === 0 || targetRect.height === 0) {
  529. throw new Error(`Target "${targetElement.textContent.trim()}" has no size.`);
  530. }
  531. // Calculate center of the target element relative to the viewport
  532. const dropClientX = targetRect.left + targetRect.width / 2;
  533. const dropClientY = targetRect.top + targetRect.height / 2;
  534.  
  535. // Convert to absolute document coordinates for potential markers (though less crucial now)
  536. const dropAbsoluteX = dropClientX + window.scrollX;
  537. const dropAbsoluteY = dropClientY + window.scrollY;
  538.  
  539. // *** Simulate Dragging Source Onto Target Center ***
  540. await this.simulateDrag(sourceElement, dropAbsoluteX, dropAbsoluteY, 'rgba(50, 150, 255, 0.7)'); // Single drag
  541. }
  542.  
  543. async simulateDrag(element, dropAbsoluteX, dropAbsoluteY, markerColor) {
  544. if (!this.isRunning || !element || !document.body.contains(element)) {
  545. console.warn("[AutoCombo] simulateDrag: Element invalid or drag stopped.");
  546. return;
  547. }
  548.  
  549. const rect = element.getBoundingClientRect();
  550. if (rect.width === 0 || rect.height === 0) {
  551. throw new Error(`Dragged elem "${element.textContent.trim()}" has no size.`);
  552. }
  553.  
  554. const clientStartX = rect.left + rect.width / 2;
  555. const clientStartY = rect.top + rect.height / 2;
  556. // Convert absolute drop coordinates to client coords for events
  557. const clientDropX = dropAbsoluteX - window.scrollX;
  558. const clientDropY = dropAbsoluteY - window.scrollY;
  559.  
  560. // Show markers at absolute positions (optional)
  561. this.showDebugMarker(clientStartX + window.scrollX, clientStartY + window.scrollY, markerColor);
  562. this.showDebugMarker(dropAbsoluteX, dropAbsoluteY, markerColor);
  563.  
  564. try {
  565. // Mouse Down on Source Element
  566. element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, button: 0, clientX: clientStartX, clientY: clientStartY, buttons: 1 }));
  567. await new Promise(res => setTimeout(res, CONFIG.dragStepDelay));
  568. if (!this.isRunning) return;
  569.  
  570. // Mouse Move to Target Element Center
  571. document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, view: window, clientX: clientDropX, clientY: clientDropY, movementX: clientDropX - clientStartX, movementY: clientDropY - clientStartY, buttons: 1 }));
  572. await new Promise(res => setTimeout(res, CONFIG.dragStepDelay));
  573. if (!this.isRunning) return;
  574.  
  575. // Mouse Up (Dispatch on document works for Infinite Craft)
  576. document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, button: 0, clientX: clientDropX, clientY: clientDropY, buttons: 0 }));
  577. console.log(`[AutoCombo] Dragged ${element.textContent.trim()} onto approx (${Math.round(clientDropX)}, ${Math.round(clientDropY)})`);
  578.  
  579. } catch (error) {
  580. console.error('[AutoCombo] Error during drag simulation step:', error);
  581. throw new Error(`Drag sim failed: ${error.message}`);
  582. }
  583. }
  584.  
  585. // --- UI & Suggestions (Mostly Unchanged) ---
  586.  
  587. _updateSuggestions() {
  588. if (!this.targetInput || !this.suggestionBox) return;
  589. const query = this.targetInput.value.toLowerCase();
  590. if (!query) {
  591. this.suggestions = []; this.suggestionBox.style.display = 'none'; return;
  592. }
  593. const currentItems = Array.from(this.itemElementMap.keys());
  594. this.suggestions = currentItems
  595. .filter(name => name.toLowerCase().includes(query))
  596. .sort((a, b) => {
  597. const aI = a.toLowerCase().indexOf(query), bI = b.toLowerCase().indexOf(query);
  598. if (aI !== bI) return aI - bI; return a.localeCompare(b);
  599. })
  600. .slice(0, CONFIG.suggestionLimit);
  601. this.suggestionIndex = -1;
  602. this._updateSuggestionUI();
  603. }
  604.  
  605. _updateSuggestionUI() {
  606. if (!this.targetInput || !this.suggestionBox) return;
  607. this.suggestionBox.innerHTML = '';
  608. if (!this.suggestions.length) { this.suggestionBox.style.display = 'none'; return; }
  609.  
  610. const inputRect = this.targetInput.getBoundingClientRect();
  611. Object.assign(this.suggestionBox.style, {
  612. position: 'absolute', display: 'block',
  613. top: `${inputRect.bottom + window.scrollY}px`, left: `${inputRect.left + window.scrollX}px`,
  614. width: `${inputRect.width}px`, maxHeight: '160px', overflowY: 'auto', zIndex: CONFIG.suggestionZIndex,
  615. });
  616.  
  617. this.suggestions.forEach((name, index) => {
  618. const div = document.createElement('div');
  619. div.textContent = name; div.className = CONFIG.suggestionItemClass; div.title = name;
  620. div.addEventListener('mousedown', (e) => { e.preventDefault(); this._handleSuggestionSelection(name); });
  621. this.suggestionBox.appendChild(div);
  622. });
  623. this._updateSuggestionHighlight(); // Includes scroll into view
  624. }
  625.  
  626. _handleSuggestionKey(e) {
  627. if (!this.suggestionBox || this.suggestionBox.style.display !== 'block' || !this.suggestions.length) {
  628. if (e.key === CONFIG.keyEnter) { e.preventDefault(); this.startAutoCombo(); } return;
  629. }
  630. const numSuggestions = this.suggestions.length;
  631. switch (e.key) {
  632. case CONFIG.keyArrowDown: case CONFIG.keyTab:
  633. e.preventDefault(); this.suggestionIndex = (this.suggestionIndex + 1) % numSuggestions; this._updateSuggestionHighlight(); break;
  634. case CONFIG.keyArrowUp:
  635. e.preventDefault(); this.suggestionIndex = (this.suggestionIndex - 1 + numSuggestions) % numSuggestions; this._updateSuggestionHighlight(); break;
  636. case CONFIG.keyEnter:
  637. e.preventDefault();
  638. if (this.suggestionIndex >= 0) this._handleSuggestionSelection(this.suggestions[this.suggestionIndex]);
  639. else { this.suggestionBox.style.display = 'none'; this.startAutoCombo(); } break;
  640. case 'Escape':
  641. e.preventDefault(); this.suggestionBox.style.display = 'none'; break;
  642. }
  643. }
  644.  
  645. _updateSuggestionHighlight() {
  646. if (!this.suggestionBox) return;
  647. Array.from(this.suggestionBox.children).forEach((child, i) => child.classList.toggle('highlighted', i === this.suggestionIndex));
  648. this._scrollSuggestionIntoView();
  649. }
  650.  
  651. _scrollSuggestionIntoView() {
  652. if (!this.suggestionBox) return;
  653. const highlightedItem = this.suggestionBox.querySelector(`.${CONFIG.suggestionItemClass}.highlighted`);
  654. if (highlightedItem) {
  655. setTimeout(() => highlightedItem.scrollIntoView?.({ block: 'nearest' }), CONFIG.suggestionHighlightDelay);
  656. }
  657. }
  658.  
  659. _handleSuggestionSelection(name) {
  660. if (!this.targetInput || !this.suggestionBox) return;
  661. this.targetInput.value = name; this.suggestionBox.style.display = 'none';
  662. this.suggestions = []; this.targetInput.focus();
  663. // Maybe auto-start here? e.g.: setTimeout(() => this.startAutoCombo(), 50);
  664. }
  665.  
  666. // --- Event Handlers (Removed Canvas Click) ---
  667. // _handleCanvasClick REMOVED
  668.  
  669. // --- Utilities ---
  670.  
  671. getElement(name) { return this.itemElementMap.get(name) || null; }
  672.  
  673. showDebugMarker(x, y, color = 'red', duration = CONFIG.debugMarkerDuration) {
  674. const dot = document.createElement('div');
  675. dot.className = CONFIG.debugMarkerClass;
  676. Object.assign(dot.style, {
  677. top: `${y - 5}px`, left: `${x - 5}px`, backgroundColor: color, position: 'absolute', opacity: '0.8'
  678. });
  679. document.body.appendChild(dot);
  680. setTimeout(() => {
  681. dot.style.opacity = '0'; // Start fade out
  682. setTimeout(() => dot.remove(), 500); // Remove after fade
  683. }, duration - 500); // Start fading before full duration
  684. }
  685.  
  686. logStatus(msg, type = 'info') {
  687. if (!this.statusBox) { console.log('[AutoCombo Status]', msg); return; }
  688. this.statusBox.textContent = msg;
  689. this.statusBox.className = `${CONFIG.statusBoxId}`; // Reset classes
  690. if (type !== 'info') this.statusBox.classList.add(`status-${type}`);
  691. if (type !== 'info' && type !== 'status-running' || !this.isRunning) {
  692. console.log(`[AutoCombo Status - ${type}]`, msg);
  693. }
  694. }
  695.  
  696. _getComboKey(name1, name2) { return [name1, name2].sort().join('||'); }
  697. }
  698.  
  699. // --- Initialization ---
  700. // Basic check to prevent multiple instances if script injected twice
  701. if (window.infCraftAutoComboInstance) {
  702. console.warn("[AutoCombo] Instance already running. Aborting new init.");
  703. } else {
  704. console.log("[AutoCombo] Creating new instance...");
  705. // document-idle should mean DOM is ready, no need for DOMContentLoaded check
  706. window.infCraftAutoComboInstance = new AutoTargetCombo();
  707. }
  708.  
  709. })();