// ==UserScript== // @name Milkyway Idle - Current Loot Tracker // @namespace https://milkywayidle.com/ // @version 1.7 // @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 // @downloadURL none // ==/UserScript== (function () { const playerLootData = {}; const previousLootCounts = {}; const lootSnapshot = {}; const lastBattleLoot = {}; let myPlayerName = null; let activePlayer = null; let selfTabSelected = false; let isMinimized = localStorage.getItem("lootOverlayMinimized") === "true"; let overlayReady = false; let hasSnapshot = 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 = `
📦 Current Loot
`; 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; } @keyframes lootFlashText { 0% { color: #b6ffb8; } 100% { color: white; } } .flashLoot { animation: lootFlashText 1.2s ease-in-out; } .fadeGain { color: lime; font-weight: bold; font-size: 10px; vertical-align: super; opacity: 1; transition: opacity 2s ease-out; margin-left: 2px; } `; 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 = () => { if (!activePlayer || !playerLootData[activePlayer]) return; const csv = Object.entries(playerLootData[activePlayer]) .map( ([hrid, count]) => `"${hrid.replace("/items/", "").replace(/_/g, " ")}",${count}` ) .join("\n"); navigator.clipboard.writeText(csv); }; document.getElementById("lootClearBtn").onclick = () => { for (const p in playerLootData) { playerLootData[p] = {}; previousLootCounts[p] = {}; lastBattleLoot[p] = {}; } 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 sorted = Object.entries(playerLootData[playerName]).sort( (a, b) => b[1] - a[1] || a[0].localeCompare(b[0]) ); let html = ""; sorted.forEach(([itemHrid, count]) => { const prev = previousLootCounts[playerName][itemHrid] || 0; const delta = count - (lastBattleLoot[playerName]?.[itemHrid] || 0); const flash = count > prev; const name = itemHrid.replace("/items/", "").replace(/_/g, " "); const gainHTML = delta > 0 ? `+${delta}` : ""; html += `
• ${name} × ${count}${gainHTML}
`; previousLootCounts[playerName][itemHrid] = count; }); container.innerHTML = html; container.style.display = "block"; document.querySelectorAll(".fadeGain").forEach((el) => { setTimeout(() => { el.style.opacity = "0"; }, 1000); }); } 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] = {}; if (!lootSnapshot[playerName]) lootSnapshot[playerName] = {}; if (!lastBattleLoot[playerName]) lastBattleLoot[playerName] = {}; for (const key in lootMap) { const { itemHrid, count } = lootMap[key]; 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") { window.dispatchEvent(new CustomEvent("LootTrackerBattle", { detail: data })); } } catch {} }); const originalSend = this.send; this.send = function (...sendArgs) { try { const data = JSON.parse(sendArgs[0]); if ( data.type === "new_character_action" && data.newCharacterActionData?.shouldClearQueue && data.newCharacterActionData.actionHrid?.startsWith("/actions/combat/") ) { window.dispatchEvent(new CustomEvent("LootTrackerCombatReset")); } } catch {} return originalSend.apply(this, sendArgs); }; } }; })(); `; document.documentElement.appendChild(s); })(); window.addEventListener("LootTrackerBattle", (e) => { const data = e.detail; data.players.forEach((player) => { const name = player.name; // Save snapshot BEFORE updating loot if (!lastBattleLoot[name]) lastBattleLoot[name] = {}; for (const key in player.totalLootMap || {}) { const { itemHrid, count } = player.totalLootMap[key]; lastBattleLoot[name][itemHrid] = playerLootData[name]?.[itemHrid] || 0; } addTab(player); if (name === activePlayer) updateLootDisplay(name); }); }); window.addEventListener("LootTrackerCombatReset", () => { for (const p in playerLootData) { playerLootData[p] = {}; previousLootCounts[p] = {}; lootSnapshot[p] = {}; lastBattleLoot[p] = {}; } hasSnapshot = false; document.getElementById("lootTabs").innerHTML = ""; document.getElementById("lootTotals").innerHTML = ""; activePlayer = null; selfTabSelected = false; }); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { createOverlay(); detectPlayerName(); }); } else { createOverlay(); detectPlayerName(); } })();