Greasy Fork

Milkyway Idle - Current Loot Tracker

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

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

// ==UserScript==
// @name         Milkyway Idle - Current Loot Tracker
// @namespace    https://milkywayidle.com/
// @version      1.2
// @description  Tracks loot with overlay, group combat loot tabs, 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;
  let overlayReady = false;
  let isMinimized = localStorage.getItem("lootOverlayMinimized") === "true";
  let myPlayerName = null;
  let activePlayer = null;
  let selfTabSelected = false;
  const playerLootData = {};

  function detectPlayerName() {
    const nameDiv = document.querySelector(
      ".CharacterName_name__1amXp[data-name]"
    );
    if (nameDiv) {
      myPlayerName = nameDiv.dataset.name;
    } else {
      setTimeout(detectPlayerName, 500);
    }
  }

  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;">📦 Current Loot</span>
        <div>
          <button id="lootExportBtn" data-tooltip="Export loot as CSV" style="background:none;border:none;color:#aaa;cursor:pointer;margin-right:6px;">CSV</button>
          <button id="lootClearBtn" data-tooltip="Clear all loot history" 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="lootTabs" style="display:flex;padding:4px 10px;gap:6px;border-bottom:1px solid #333;background:rgba(24,24,24,0.8);"></div>
      <div id="lootTotals" style="padding:10px;display:${isMinimized ? "none" : "block"};max-height:400px;overflow-y:auto;"></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;
          }
          #lootTabs button {
            background: none;
            border: 1px solid #444;
            color: #aaa;
            padding: 2px 6px;
            font-family: monospace;
            cursor: pointer;
            border-radius: 4px;
            font-size: 12px;
          }
          #lootTabs button.active {
            background: #4caf50;
            color: #fff;
            border-color: #4caf50;
          }
          #lootTotals::-webkit-scrollbar {
            width: 6px;
          }
          #lootTotals::-webkit-scrollbar-track {
            background: rgba(0,0,0,0.1);
          }
          #lootTotals::-webkit-scrollbar-thumb {
            background: rgba(255,255,255,0.2);
            border-radius: 3px;
          }
          .button-active {
            animation: buttonPulse 0.5s ease-out;
          }
          @keyframes buttonPulse {
            0% { background: rgba(76, 175, 80, 0); }
            50% { background: rgba(76, 175, 80, 0.3); }
            100% { background: rgba(76, 175, 80, 0); }
          }
        </style>
      `
    );

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

    // Export to CSV button
    document.getElementById("lootExportBtn").onclick = function() {
      if (!activePlayer || !playerLootData[activePlayer]) return;

      const csvContent = Object.entries(playerLootData[activePlayer])
        .map(([itemHrid, count]) => {
          const itemName = itemHrid.replace("/items/", "").replace(/_/g, " ");
          return `"${itemName}",${count}`;
        })
        .join("\n");

      navigator.clipboard.writeText(csvContent);
      this.classList.add("button-active");
      setTimeout(() => this.classList.remove("button-active"), 500);
    };

    // Clear history button
    document.getElementById("lootClearBtn").onclick = function() {
      // Clear all player data
      for (const player in playerLootData) {
        playerLootData[player] = {};
      }

      // Clear tabs
      const tabsContainer = document.getElementById("lootTabs");
      tabsContainer.innerHTML = "";
      activePlayer = null;
      selfTabSelected = false;

      // Clear display
      document.getElementById("lootTotals").innerHTML = "";

      // Visual feedback
      this.classList.add("button-active");
      setTimeout(() => this.classList.remove("button-active"), 500);
    };

    // Make panel draggable
    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 updateLootDisplay(playerName) {
    const container = document.getElementById("lootTotals");
    if (!container || !playerLootData[playerName]) return;

    const sortedItems = Object.entries(playerLootData[playerName])
      .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));

    container.innerHTML = sortedItems
      .map(([itemHrid, count]) => {
        const name = itemHrid.replace("/items/", "").replace(/_/g, " ");
        return `<div>• ${name} × ${count}</div>`;
      })
      .join("");
  }

  function switchTab(playerName) {
    activePlayer = playerName;
    document.querySelectorAll("#lootTabs button").forEach((btn) => {
      btn.classList.toggle("active", btn.textContent === playerName);
    });
    updateLootDisplay(playerName);
  }

  function addTab(player) {
    const playerName = player.name;
    const container = document.getElementById("lootTabs");

    if (!container.querySelector(`button[data-name="${playerName}"]`)) {
      const btn = document.createElement("button");
      btn.textContent = playerName;
      btn.dataset.name = playerName;
      btn.onclick = () => switchTab(playerName);
      container.appendChild(btn);
    }

    // Store the player's loot data
    const lootMap = player.totalLootMap || {};
    playerLootData[playerName] = {};

    for (const key in lootMap) {
      const item = lootMap[key];
      playerLootData[playerName][item.itemHrid] = item.count;
    }

    // Auto-select our own tab if we haven't selected one yet
    if (playerName === myPlayerName && !selfTabSelected) {
      selfTabSelected = true;
      switchTab(playerName);
    }
  }

  // Initialize
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      createOverlay();
      detectPlayerName();
    });
  } else {
    createOverlay();
    detectPlayerName();
  }

  // WebSocket handling
  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;

        (data.players || []).forEach(player => {
          addTab(player);

          if (player.name === activePlayer) {
            updateLootDisplay(activePlayer);
          }
        });
      } catch (e) {
        console.error("Error processing WebSocket message:", e);
      }
    });

    return ws;
  };

  window.WebSocket.prototype = originalWS.prototype;
})();