InfiniteCraft: Autocraft + Recipe Search

Adds draggable combo log for better ad overlay, autocrafting, lineage tracing, and combo search for Infinite Craft

// ==UserScript==
// @name         InfiniteCraft: Autocraft + Recipe Search
// @namespace    http://tampermonkey.net/
// @version      6.2.1
// @description  Adds draggable combo log for better ad overlay, autocrafting, lineage tracing, and combo search for Infinite Craft
// @author       Gasclu
// @match        https://neal.fun/infinite-craft/
// @match        https://beta.neal.fun/infinite-craft/
// @grant        none
// @run-at       document-end
// @license      MIT 
// ==/UserScript==

(function () {
  "use strict";

  const lineageKey = "autocrafter_lineages";

  const AT = {
    infinitecraft: null,
    isCrafting: false,

    getLineages() {
      return JSON.parse(localStorage.getItem(lineageKey) || "{}");
    },

    saveLineage(a, b, result) {
      const lineages = AT.getLineages();
      lineages[result] = [a, b];
      localStorage.setItem(lineageKey, JSON.stringify(lineages));
    },

    traceSteps(result) {
      const lineages = AT.getLineages();
      const steps = [];
      const visited = new Set();

      function walk(item) {
        if (visited.has(item)) return;
        visited.add(item);
        const inputs = lineages[item];
        if (!inputs) return;
        const [a, b] = inputs;
        walk(a);
        walk(b);
        steps.push({ a, b, result: item });
      }

      walk(result);
      return steps;
    },

    showLineagePanel(result) {
      const steps = AT.traceSteps(result);
      if (steps.length === 0) return AT.showToast(`No lineage for "${result}"`);

      const container = document.createElement("div");
      container.id = "lineage-panel";
      container.style = `
        position: fixed; top: 10%; left: 50%; transform: translateX(-50%);
        background: #1e1e1e; color: white; padding: 20px; border-radius: 10px;
        max-width: 500px; max-height: 70%; overflow-y: auto; z-index: 99999;
        box-shadow: 0 0 15px #000;
      `;

      const lineageHTML = steps.map(({ a, b, result }) => `
        <li class="lineage-step" data-result="${result}">
          <span class="clickable-element" data-name="${a}">${a}</span> +
          <span class="clickable-element" data-name="${b}">${b}</span> →
          <strong class="clickable-element" data-name="${result}">${result}</strong>
        </li>
      `).join("");

      container.innerHTML = `
        <h2 style="margin-top: 0;">📘 How to craft <span style="color:#0f0">${result}</span></h2>
        <ol style="line-height: 1.6;" id="lineage-steps">
          ${lineageHTML}
        </ol>
        <button style="margin-top:10px;" onclick="this.parentElement.remove()">Close</button>
      `;
      document.body.appendChild(container);

      const allSteps = Array.from(container.querySelectorAll(".lineage-step"));

      function highlightLineage(target) {
        const relatedResults = new Set(AT.traceSteps(target).map(s => s.result));
        allSteps.forEach(step => {
          const r = step.dataset.result;
          const isRelevant = relatedResults.has(r);
          step.style.background = isRelevant ? "#003300" : "transparent";
          step.style.color = isRelevant ? "lime" : "white";

          step.querySelectorAll(".clickable-element").forEach(el => {
            el.style.color = (isRelevant && el.textContent === target) ? "lime" : (isRelevant ? "#0f0" : "white");
          });
        });
      }

      container.querySelectorAll('.clickable-element').forEach(el => {
        el.style.cursor = 'pointer';
        el.style.color = '#0f0';
        el.onclick = () => {
          highlightLineage(el.dataset.name);
        };
      });
    },

    showComboSearch() {
      const div = document.createElement("div");
      div.id = "combo-search";
      div.innerHTML = `
        <div style="
          position: fixed; top: 30%; left: 50%; transform: translateX(-50%);
          background: linear-gradient(to bottom right, #3c763d, #2e4d2e);
          color: white; padding: 20px; border-radius: 10px; text-align: center;
          box-shadow: 0 0 20px #000; z-index: 99999; width: 320px;
        ">
          <h3>🔍 Combo Search</h3>
          <input id="combo-input" type="text" placeholder="Enter element..." style="
            width: 90%; padding: 8px; border-radius: 5px; border: none; margin-bottom: 10px;
            background: #f0fff0; color: black; text-align: center;
          " />
          <div id="combo-results" style="text-align:left; max-height:150px; overflow-y:auto; background:#f0fff0; color:black; padding:5px; border-radius:5px;"></div>
          <button onclick="document.getElementById('combo-search').remove()">Close</button>
        </div>
      `;
      document.body.appendChild(div);

      const input = div.querySelector("#combo-input");
      const results = div.querySelector("#combo-results");

      input.addEventListener("input", () => {
        const q = input.value.toLowerCase();
        results.innerHTML = "";

        const matches = [];
        const lineage = AT.getLineages();
        for (let [result, [a, b]] of Object.entries(lineage)) {
          if (a.toLowerCase().includes(q) || b.toLowerCase().includes(q)) {
            matches.push(`${a} + ${b} → <strong>${result}</strong>`);
          }
        }

        results.innerHTML = matches.length === 0
          ? "<i>No matches found</i>"
          : "<ul style='padding-left:20px'>" + matches.map(m => `<li>${m}</li>`).join("") + "</ul>";
      });
    },

    showSearchModal() {
      if (document.getElementById("earth-modal")) return;

      const div = document.createElement("div");
      div.id = "earth-modal";
      div.innerHTML = `
        <div style="
          position: fixed; top: 30%; left: 50%; transform: translateX(-50%);
          background: linear-gradient(to bottom right, #3c763d, #2e4d2e);
          color: white; padding: 20px; border-radius: 10px; text-align: center;
          box-shadow: 0 0 20px #000; z-index: 99999; width: 300px;
        ">
          <h3>🌍 Lineage Search</h3>
          <input id="lineage-input" type="text" autocomplete="off" placeholder="Enter element..." style="
            width: 90%; padding: 8px; border-radius: 5px; border: none; margin-bottom: 10px;
            background: #f0fff0; color: black; text-align: center;
          " />
          <ul id="autocomplete-list" style="
            list-style: none; padding: 0; margin: 0 auto 10px; max-height: 100px;
            overflow-y: auto; background: #f0fff0; color: black; border-radius: 5px;
            width: 90%; text-align: left; font-size: 14px;
          "></ul>
          <button id="lineage-show">Show</button>
          <button onclick="document.getElementById('earth-modal').remove()">Cancel</button>
        </div>
      `;
      document.body.appendChild(div);

      const input = document.getElementById("lineage-input");
      const list = document.getElementById("autocomplete-list");

      input.addEventListener("input", () => {
        const val = input.value.toLowerCase();
        list.innerHTML = "";
        if (!val) return;

        const matches = AT.infinitecraft.items
          .map(i => i.text)
          .filter(name => name.toLowerCase().includes(val))
          .sort((a, b) => a.toLowerCase().indexOf(val) - b.toLowerCase().indexOf(val))
          .slice(0, 8);

        for (let name of matches) {
          const li = document.createElement("li");
          li.textContent = name;
          li.style.padding = "5px 10px";
          li.style.cursor = "pointer";
          li.onclick = () => {
            input.value = name;
            list.innerHTML = "";
          };
          list.appendChild(li);
        }
      });

      document.getElementById("lineage-show").onclick = () => {
        const name = input.value.trim();
        if (name) AT.showLineagePanel(name);
        div.remove();
      };
    },

    initComboLog() {
      let log = document.getElementById("combo-log");
      if (!log) {
        log = document.createElement("div");
        log.id = "combo-log";
        log.style = `
          position: fixed;
          bottom: 0;
          left: 50%;
          transform: translateX(-50%);
          background: rgba(0,0,0,0.95);
          color: white;
          padding: 10px;
          max-height: 200px;
          overflow-y: auto;
          font-family: monospace;
          font-size: 13px;
          border-radius: 8px 8px 0 0;
          z-index: 99999;
          width: 336px;
          cursor: move;
        `;
        log.innerHTML = `<div id="combo-log-content" style="max-height: 180px; overflow-y: auto;"></div>`;
        document.body.appendChild(log);

        // Draggable support
        let isDragging = false, startX = 0, startLeft = 0;
        log.addEventListener("mousedown", e => {
          isDragging = true;
          startX = e.clientX;
          startLeft = log.offsetLeft;
          e.preventDefault();
        });
        document.addEventListener("mousemove", e => {
          if (isDragging) {
            const deltaX = e.clientX - startX;
            log.style.left = `${startLeft + deltaX}px`;
            log.style.transform = "translateX(0)";
          }
        });
        document.addEventListener("mouseup", () => {
          isDragging = false;
        });
      }
    },

    logCombo(a, b, result, success) {
      AT.initComboLog();
      const div = document.createElement("div");
      div.innerHTML = `${a} + ${b} → <strong>${result || "Nothing"}</strong>`;
      div.style.color = success ? "lime" : "red";
      document.getElementById("combo-log-content").appendChild(div);
      const log = document.getElementById("combo-log-content");
      log.scrollTop = log.scrollHeight;
    },

    showToast(msg) {
      const div = document.createElement("div");
      div.textContent = msg;
      Object.assign(div.style, {
        position: "fixed", bottom: "20px", left: "50%", transform: "translateX(-50%)",
        background: "#444", color: "white", padding: "10px 20px", borderRadius: "6px",
        zIndex: 10000, opacity: 0, transition: "opacity 0.3s ease"
      });
      document.body.appendChild(div);
      setTimeout(() => div.style.opacity = 1, 10);
      setTimeout(() => {
        div.style.opacity = 0;
        setTimeout(() => div.remove(), 300);
      }, 2500);
    },

    monitorCrafts() {
      const orig = AT.infinitecraft.craft;
      AT.infinitecraft.craft = async (a, b) => {
        const result = await orig.call(AT.infinitecraft, a, b);
        const last = AT.infinitecraft.items.at(-1);
        if (last && last.text) {
          AT.saveLineage(a.text, b.text, last.text);
        }
        return result;
      };
    },

    startAutoCraft() {
      const loop = async () => {
        while (AT.isCrafting) {
          const items = AT.infinitecraft.items;
          const a = items[Math.floor(Math.random() * items.length)].text;
          const b = items[Math.floor(Math.random() * items.length)].text;
          const before = items.length;
          await AT.infinitecraft.craft({ text: a }, { text: b });
          const after = AT.infinitecraft.items.length;
          const result = AT.infinitecraft.items.at(-1);
          const success = after > before;
          if (success && result?.text) {
            AT.saveLineage(a, b, result.text);
          }
          AT.logCombo(a, b, success ? result?.text : null, success);
          AT.infinitecraft.instances = [];
          await new Promise(r => setTimeout(r, 200));
        }
      };
      loop();
    },

    addButtons() {
      const controls = document.querySelector(".side-controls");
      if (!controls) return setTimeout(AT.addButtons, 200);

      const addBtn = (text, action) => {
        const btn = document.createElement("div");
        btn.textContent = text;
        btn.className = "tool-icon";
        btn.style.cssText = `
          font-size: 13px;
          padding: 6px 10px;
          background: #3a5f3a;
          color: white;
          border-radius: 6px;
          margin-top: 8px;
          text-align: center;
          cursor: pointer;
        `;
        btn.onclick = action;
        controls.appendChild(btn);
      };

      const icon = document.createElement("img");
      icon.src = "https://i.imgur.com/WlkWOkU.png";
      icon.classList.add("tool-icon");
      icon.style.width = "32px";
      icon.style.height = "32px";
      icon.style.filter = "brightness(1)";
      icon.title = "Auto-Crafter";
      icon.onclick = () => {
        AT.isCrafting = !AT.isCrafting;
        icon.style.filter = AT.isCrafting
          ? "drop-shadow(0 0 5px lime) brightness(1.5)"
          : "brightness(1)";
        if (AT.isCrafting) AT.startAutoCraft();
      };
      controls.appendChild(icon);

      addBtn("🌍 Lineage Search", AT.showSearchModal);
      addBtn("🔍 Combo Search", AT.showComboSearch);
    },

    start() {
      const vue = document.querySelector(".container")?.__vue__;
      if (!vue) return setTimeout(AT.start, 200);
      AT.infinitecraft = vue;
      AT.addButtons();
      AT.monitorCrafts();
      console.log("✅ InfiniteCraft v5.9 Loaded with draggable combo log!");
    }
  };

  window.AT = AT;
  AT.start();
})();
;

长期地址
遇到问题?请前往 GitHub 提 Issues。