// ==UserScript==
// @name Milkyway Idle - Current Loot Tracker
// @namespace https://milkywayidle.com/
// @version 2.0
// @description Displays real-time loot totals and estimated coin value in a compact, draggable overlay with tabbed player view
// @match https://www.milkywayidle.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
const playerLootData = {};
const previousLootCounts = {};
const lastBattleLoot = {};
let myPlayerName = null;
let activePlayer = null;
let selfTabSelected = false;
let isMinimized = localStorage.getItem("lootOverlayMinimized") === "true";
let overlayReady = false;
let isLootListMinimized = false;
let marketData = {};
fetch(
"https://raw.githubusercontent.com/holychikenz/MWIApi/main/medianmarket.json"
)
.then((r) => r.json())
.then((data) => {
marketData = data;
})
.catch((err) =>
console.error("[LootTracker] Failed to load market data:", err)
);
function formatGold(value) {
return Math.round(value).toLocaleString() + " coin";
}
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 Everything" style="background:none;border:none;color:#aaa;cursor:pointer;margin-right:6px;">
${isMinimized ? "+" : "−"}
</button>
</div>
</div>
<div id="lootContent" style="
overflow: hidden;
transition: max-height 0.3s ease, opacity 0.3s ease;
max-height: ${isMinimized ? "0" : "1000px"};
opacity: ${isMinimized ? "0" : "1"};
">
<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="lootToggleHeader" style="padding:6px 10px;cursor:pointer;font-weight:bold;border-bottom:1px solid #333;">
Loot <span id="lootToggleIcon">${
isLootListMinimized ? "▲" : "▼"
}</span>
</div>
<div id="lootTotals" style="
padding:10px;
overflow-y:auto;
transition: max-height 0.3s ease, opacity 0.3s ease;
max-height: ${isLootListMinimized ? "0" : "400px"};
opacity: ${isLootListMinimized ? "0" : "1"};
"></div>
<div id="lootBottomDragger" style="padding:6px;cursor:move;border-top:1px solid #444">
<div id="lootRevenueLine" style="font-weight:bold;color:gold;cursor:inherit"></div>
<div style="height:8px;cursor:inherit;"></div>
</div>
</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;
}
@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;
}
#lootContent {
transition: max-height 0.3s ease, opacity 0.3s ease;
overflow: hidden;
will-change: max-height, opacity;
}
`;
document.head.appendChild(style);
// Button actions
document.getElementById("lootMinBtn").onclick = () => {
isMinimized = !isMinimized;
const content = document.getElementById("lootContent");
content.style.maxHeight = isMinimized ? "0" : "1000px";
content.style.opacity = isMinimized ? "0" : "1";
document.getElementById("lootMinBtn").textContent = isMinimized
? "+"
: "−";
localStorage.setItem("lootOverlayMinimized", isMinimized);
};
document.getElementById("lootToggleHeader").onclick = () => {
isLootListMinimized = !isLootListMinimized;
const lootTotals = document.getElementById("lootTotals");
lootTotals.style.maxHeight = isLootListMinimized ? "0" : "400px";
lootTotals.style.opacity = isLootListMinimized ? "0" : "1";
lootTotals.style.padding = isLootListMinimized ? "0" : "10px";
const icon = document.getElementById("lootToggleIcon");
icon.textContent = isLootListMinimized ? "▲" : "▼";
};
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;
};
// Dragging
let dragging = false;
let offsetX = 0;
let offsetY = 0;
function beginDrag(e) {
if (e.target.tagName === "BUTTON") return;
dragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
document.body.style.userSelect = "none";
}
document
.getElementById("lootHeader")
.addEventListener("mousedown", beginDrag);
function tryAddBottomDragger() {
const bottomDragger = document.getElementById("lootBottomDragger");
if (bottomDragger) {
bottomDragger.addEventListener("mousedown", beginDrag);
} else {
setTimeout(tryAddBottomDragger, 100);
}
}
tryAddBottomDragger();
document.addEventListener("mousemove", (e) => {
if (!dragging) return;
panel.style.left = `${e.clientX - offsetX}px`;
panel.style.top = `${e.clientY - offsetY}px`;
});
document.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
localStorage.setItem("lootOverlayTop", panel.style.top);
localStorage.setItem("lootOverlayLeft", panel.style.left);
document.body.style.userSelect = "";
});
}
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 = "";
let totalRevenue = 0;
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 ? `<span class="fadeGain">+${delta}</span>` : "";
// Revenue calculation
const marketKey = name
.split(" ")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ");
const market = marketData.market?.[marketKey];
if (itemHrid.endsWith("/coin")) {
totalRevenue += count;
} else if (market?.ask) {
totalRevenue += count * market.ask;
}
html += `<div class="${flash ? "flashLoot" : ""}" style="color:white">
• ${name} × ${count}${gainHTML}
</div>`;
previousLootCounts[playerName][itemHrid] = count;
});
document.getElementById(
"lootRevenueLine"
).textContent = `Total Value: ${formatGold(totalRevenue)}`;
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 (!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 = new Proxy(originalWebSocket, {
construct(target, args) {
const ws = new target(...args);
if (
ws.url.includes("api.milkywayidle.com/ws") ||
ws.url.includes("api-test.milkywayidle.com/ws")
) {
ws.addEventListener("message", (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === "new_battle") {
window.dispatchEvent(new CustomEvent("LootTrackerBattle", { detail: data }));
} else if (
data.type === "new_character_action" &&
data.newCharacterActionData?.shouldClearQueue &&
data.newCharacterActionData.actionHrid?.startsWith("/actions/combat/")
) {
window.dispatchEvent(new CustomEvent("LootTrackerCombatReset"));
}
} catch {}
});
}
return ws;
}
});
})();
`;
document.documentElement.appendChild(s);
})();
window.addEventListener("LootTrackerBattle", (e) => {
const data = e.detail;
data.players.forEach((player) => {
const name = player.name;
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] = {};
lastBattleLoot[p] = {};
}
document.getElementById("lootTabs").innerHTML = "";
document.getElementById("lootTotals").innerHTML = "";
activePlayer = null;
selfTabSelected = false;
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
createOverlay();
detectPlayerName();
});
} else {
createOverlay();
detectPlayerName();
}
})();