Greasy Fork

Milkyway Idle - Loot Tracker

Tracks loot with overlay, visual feedback, CSV export, clear log, persistent layout

目前为 2025-03-30 提交的版本。查看 最新版本

// ==UserScript==
// @name         Milkyway Idle - Loot Tracker
// @namespace    https://milkywayidle.com/
// @version      1.1
// @description  Tracks loot with overlay, visual feedback, CSV export, clear log, persistent layout
// @match        https://www.milkywayidle.com/*
// @grant        none
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function () {
  const originalWS = window.WebSocket;
  const previousLoot = {};
  const sessionLoot = {};
  let overlayReady = false;
  let isMinimized = localStorage.getItem("lootOverlayMinimized") === "true";

  function createOverlay() {
    if (overlayReady || document.getElementById("lootOverlay")) return;
    overlayReady = true;

    const panel = document.createElement("div");
    panel.id = "lootOverlay";
    panel.style.position = "fixed";
    panel.style.top = localStorage.getItem("lootOverlayTop") || "100px";
    panel.style.left = localStorage.getItem("lootOverlayLeft") || "20px";
    panel.style.width = "260px";
    panel.style.background = "rgba(30, 30, 30, 0.95)";
    panel.style.color = "#fff";
    panel.style.fontFamily = "monospace";
    panel.style.fontSize = "13px";
    panel.style.border = "1px solid #555";
    panel.style.borderRadius = "8px";
    panel.style.zIndex = 99999;
    panel.style.userSelect = "none";
    panel.style.boxShadow = "0 4px 10px rgba(0,0,0,0.4)";

    panel.innerHTML = `
      <div id="lootHeader" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:rgba(20,20,20,0.85);border-bottom:1px solid #333;border-radius:8px 8px 0 0;cursor:move;">
        <span style="font-weight:bold;">📦 Loot Logger</span>
        <div>
          <button id="lootExportBtn" data-tooltip="Export loot summary as CSV" style="background:none;border:none;color:#aaa;cursor:pointer;margin-right:6px;">CSV</button>
          <button id="lootClearBtn" data-tooltip="Clear loot log" style="background:none;border:none;color:#aaa;cursor:pointer;margin-right:6px;">⟳</button>
          <button id="lootMinBtn" data-tooltip="Minimize/Expand" style="background:none;border:none;color:#aaa;cursor:pointer;">${isMinimized ? "+" : "−"}</button>
        </div>
      </div>
      <div id="lootTotals" style="padding:10px;display:${isMinimized ? "none" : "block"};"></div>
    `;

    document.body.appendChild(panel);

    document.head.insertAdjacentHTML('beforeend', `
      <style>
        #lootOverlay button:hover::after {
          content: attr(data-tooltip);
          position: absolute; left:50%; top:100%; transform:translateX(-50%);
          background:#222;color:#fff;padding:4px 8px;font-size:11px;
          border-radius:4px;white-space:nowrap;opacity:0.9;pointer-events:none;
          z-index:100000;margin-top:4px;
        }
        .loot-highlight { animation: highlightLoot 1s ease-out; }
        @keyframes highlightLoot {
          from { background:rgba(76, 175, 80, 0.4); }
          to { background:transparent; }
        }
      </style>
    `);

    document.getElementById("lootMinBtn").onclick = () => {
      isMinimized = !isMinimized;
      document.getElementById("lootTotals").style.display = isMinimized ? "none" : "block";
      document.getElementById("lootMinBtn").textContent = isMinimized ? "+" : "−";
      localStorage.setItem("lootOverlayMinimized", isMinimized);
    };

    document.getElementById("lootExportBtn").onclick = () => {
      const lootSummary = Object.entries(sessionLoot).map(([itemHrid, count]) =>
        `${itemHrid.replace("/items/","").replace(/_/g," ")},${count}`
      ).join("\n");
      navigator.clipboard.writeText(lootSummary);
    };

    document.getElementById("lootClearBtn").onclick = () => {
      for (let k in sessionLoot) delete sessionLoot[k];
      updateOverlay();
    };

    let dragging = false, offsetX, offsetY;
    document.getElementById("lootHeader").onmousedown = (e) => {
      if (e.target.tagName === "BUTTON") return;
      dragging = true;
      offsetX = e.clientX - panel.offsetLeft;
      offsetY = e.clientY - panel.offsetTop;
    };
    document.onmousemove = (e) => {
      if (dragging) {
        panel.style.left = `${e.clientX - offsetX}px`;
        panel.style.top = `${e.clientY - offsetY}px`;
      }
    };
    document.onmouseup = () => {
      if (dragging) {
        localStorage.setItem("lootOverlayTop", panel.style.top);
        localStorage.setItem("lootOverlayLeft", panel.style.left);
      }
      dragging = false;
    };
  }

  function updateOverlay(newItems = []) {
    const container = document.getElementById("lootTotals");
    if (!container) return;

    container.innerHTML = Object.entries(sessionLoot).map(([itemHrid, count]) => {
      const name = itemHrid.replace("/items/", "").replace(/_/g, " ");
      const highlight = newItems.includes(itemHrid) ? 'loot-highlight' : '';
      return `<div class="${highlight}">• ${name} × ${count}</div>`;
    }).join("");
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", createOverlay);
  } else {
    setTimeout(createOverlay, 0);
  }

  window.WebSocket = function (...args) {
    const ws = new originalWS(...args);
    ws.addEventListener("message", (event) => {
      try {
        const data = JSON.parse(event.data);
        if (data.type !== "new_battle") return;
        const lootMap = data.players?.[0]?.totalLootMap;
        if (!lootMap) return;
        const newItems = [];

        for (const key in lootMap) {
          const item = lootMap[key];
          const delta = item.count - (previousLoot[key]?.count || 0);
          if (delta > 0) {
            sessionLoot[item.itemHrid] = (sessionLoot[item.itemHrid] || 0) + delta;
            newItems.push(item.itemHrid);
          }
          previousLoot[key] = { ...item };
        }
        updateOverlay(newItems);
      } catch {}
    });
    return ws;
  };
  window.WebSocket.prototype = originalWS.prototype;
})();