Milkyway Idle - Current Loot Tracker

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

目前為 2025-04-02 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Milkyway Idle - Current Loot Tracker
// @namespace    https://milkywayidle.com/
// @version      1.6
// @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
// @inject-into  page
// ==/UserScript==

(function () {
  const playerLootData = {};
  const previousLootCounts = {};
  let myPlayerName = null;
  let activePlayer = null;
  let selfTabSelected = false;
  let isMinimized = localStorage.getItem("lootOverlayMinimized") === "true";
  let overlayReady = false;

  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);

    const style = document.createElement("style");
    style.textContent = `
      #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;
      }
    `;
    document.head.appendChild(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 = 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);
    };

    document.getElementById("lootClearBtn").onclick = function () {
      for (const player in playerLootData) {
        playerLootData[player] = {};
        previousLootCounts[player] = {};
      }
      document.getElementById("lootTabs").innerHTML = "";
      document.getElementById("lootTotals").innerHTML = "";
      activePlayer = null;
      selfTabSelected = false;
    };

    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;
    if (!previousLootCounts[playerName]) previousLootCounts[playerName] = {};

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

    let html = "";
    for (const [itemHrid, count] of sortedItems) {
      const name = itemHrid.replace("/items/", "").replace(/_/g, " ");
      html += `<div style="color:white">• ${name} × ${count}</div>`;
    }
    container.innerHTML = html;
    container.style.display = "block";
  }

  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 lootMap = player.totalLootMap || {};
    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);
    }

    if (!playerLootData[playerName]) {
      playerLootData[playerName] = {};
    }

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

    if (playerName === myPlayerName && !selfTabSelected) {
      selfTabSelected = true;
      switchTab(playerName);
    }
  }

(function injectImmediately() {
  const s = document.createElement('script');
  s.textContent = `
    (function() {
      const OriginalWebSocket = window.WebSocket;
      window.WebSocket = class extends OriginalWebSocket {
        constructor(...args) {
          super(...args);
          this.addEventListener("message", (event) => {
            try {
              const data = JSON.parse(event.data);
              if (data.type !== "new_battle") return;
              window.dispatchEvent(new CustomEvent("LootTrackerBattle", { detail: data }));
            } catch (err) {}
          });
        }
      };
      console.log('[LootTracker] WebSocket override injected');
    })();
  `;
  document.documentElement.appendChild(s);
})();

window.addEventListener("LootTrackerBattle", (e) => {
  const data = e.detail;
  (data.players || []).forEach((player) => {
    addTab(player);
    if (player.name === activePlayer) {
      updateLootDisplay(activePlayer);
    }
  });
});

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