// ==UserScript==
// @name Milkyway Idle - Current Loot Tracker
// @namespace https://milkywayidle.com/
// @version 1.5
// @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
// @downloadURL none
// ==/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 = `
`;
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 += `• ${name} × ${count}
`;
}
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 interceptWebSocket() {
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;
(data.players || []).forEach((player) => {
addTab(player);
if (player.name === activePlayer) {
updateLootDisplay(activePlayer);
}
});
} catch (err) {}
});
}
};
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
createOverlay();
detectPlayerName();
interceptWebSocket();
});
} else {
createOverlay();
detectPlayerName();
interceptWebSocket();
}
})();