// ==UserScript==
// @name Milkyway Idle - Current Loot Tracker
// @namespace https://milkywayidle.com/
// @version 2.1
// @description Tracks loot with overlay, total coin value via ask prices, improved UI/CSS, and fixed display logic.
// @match https://www.milkywayidle.com/*
// @grant none
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/531302/Milkyway%20Idle%20-%20Current%20Loot%20Tracker.user.js
// @updateURL https://update.greasyfork.icu/scripts/531302/Milkyway%20Idle%20-%20Current%20Loot%20Tracker.meta.js
// ==/UserScript==
(function () {
"use strict";
const playerLootData = {};
const previousLootCounts = {};
const lastBattleLoot = {};
let myPlayerName = null;
let activePlayer = null;
let selfTabSelected = false;
let isMinimized = localStorage.getItem("lootOverlayMinimized") === "true";
let isLootListMinimized =
localStorage.getItem("lootListMinimized") === "true";
let overlayReady = false;
let marketData = {};
fetch(
"https://raw.githubusercontent.com/holychikenz/MWIApi/main/medianmarket.json"
)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
marketData = data;
console.log("[LootTracker] Market data loaded successfully.");
if (activePlayer && document.getElementById("lootOverlay")) {
updateLootDisplay(activePlayer);
}
})
.catch((err) =>
console.error("[LootTracker] Failed to load market data:", err)
);
function formatGold(value) {
const numValue = Number(value) || 0;
return Math.round(numValue).toLocaleString() + " coin";
}
function detectPlayerName() {
const nameDiv =
document.querySelector(".CharacterStatus_playerName__XXXXX") ||
document.querySelector(".CharacterName_name__1amXp[data-name]");
if (nameDiv) {
myPlayerName = nameDiv.dataset.name || nameDiv.textContent.trim();
if (
overlayReady &&
myPlayerName &&
playerLootData[myPlayerName] &&
!selfTabSelected
) {
selfTabSelected = true;
switchTab(myPlayerName);
}
} else {
setTimeout(detectPlayerName, 1000);
}
}
function createOverlay() {
if (overlayReady || document.getElementById("lootOverlay")) return;
overlayReady = true;
const panel = document.createElement("div");
panel.id = "lootOverlay";
panel.style.top = localStorage.getItem("lootOverlayTop") || "100px";
panel.style.left = localStorage.getItem("lootOverlayLeft") || "20px";
panel.innerHTML = `
Total Value: Calculating...
`;
document.body.appendChild(panel);
const style = document.createElement("style");
style.textContent = `
#lootOverlay {
position: fixed;
width: 260px;
background: rgba(30, 30, 30, 0.95);
color: #fff;
font-family: monospace;
font-size: 13px;
border: 1px solid #555;
border-radius: 8px;
z-index: 99999;
user-select: none;
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
}
#lootHeader {
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;
}
#lootTitle { font-weight: bold; }
#lootHeaderButtons { display: flex; gap: 4px; }
.loot-btn {
background: none; border: none; color: #aaa; cursor: pointer;
font-size: 14px; padding: 0 3px; position: relative;
}
.loot-btn:hover { color: #fff; }
.loot-btn:hover::after {
content: attr(data-tooltip); position: absolute; left: 50%; top: 110%;
transform: translateX(-50%); background: #222; color: #fff; padding: 4px 8px;
font-size: 11px; border-radius: 4px; white-space: nowrap; opacity: 0.95;
pointer-events: none; z-index: 100000;
}
#lootContent {
overflow: hidden; transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
will-change: max-height, opacity;
}
#lootTabs {
display: flex; flex-wrap: wrap; padding: 5px 10px; gap: 6px;
border-bottom: 1px solid #333; background: rgba(24, 24, 24, 0.8); min-height: 26px;
}
#lootTabs button {
background: none; border: 1px solid #444; color: #aaa; padding: 2px 6px;
font-family: monospace; cursor: pointer; border-radius: 4px; font-size: 12px;
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
}
#lootTabs button:hover { background-color: #555; color: #fff; }
#lootTabs button.active {
background: #4caf50; color: #fff; border-color: #4caf50; font-weight: bold;
}
#lootToggleHeader {
padding: 6px 10px; cursor: pointer; font-weight: bold; border-bottom: 1px solid #333;
background: rgba(28, 28, 28, 0.8);
}
#lootToggleHeader:hover { background: rgba(40, 40, 40, 0.9); }
#lootToggleIcon { display: inline-block; transition: transform 0.2s ease-out; margin-left: 5px; }
#lootTotals {
padding: 10px; overflow-y: auto; max-height: 400px;
transition: max-height 0.3s ease-out, opacity 0.3s ease-out, padding 0.3s ease-out;
will-change: max-height, opacity, padding;
}
#lootTotals > div { margin-bottom: 3px; line-height: 1.3; }
#lootBottomDragger {
padding: 6px 10px; cursor: move; border-top: 1px solid #444;
background: rgba(20, 20, 20, 0.85); border-radius: 0 0 8px 8px;
}
#lootRevenueLine {
font-weight: bold; color: gold; cursor: inherit; padding-bottom: 4px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.drag-spacer { height: 8px; cursor: inherit; }
@keyframes lootFlashText { 0% { color: #b6ffb8; transform: scale(1.02); } 100% { color: white; transform: scale(1); } }
.flashLoot { animation: lootFlashText 1s ease-out; }
.fadeGain {
color: lime; font-weight: bold; font-size: 10px; vertical-align: super;
opacity: 1; transition: opacity 2s ease-out; margin-left: 3px; display: inline-block;
}
`;
document.head.appendChild(style);
const content = document.getElementById("lootContent");
const lootTotals = document.getElementById("lootTotals");
content.style.maxHeight = isMinimized ? "0" : "1000px";
content.style.opacity = isMinimized ? "0" : "1";
lootTotals.style.maxHeight = isLootListMinimized ? "0" : "400px";
lootTotals.style.opacity = isLootListMinimized ? "0" : "1";
lootTotals.style.padding = isLootListMinimized ? "0 10px" : "10px";
document.getElementById("lootMinBtn").onclick = () => {
isMinimized = !isMinimized;
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;
lootTotals.style.maxHeight = isLootListMinimized ? "0" : "400px";
lootTotals.style.opacity = isLootListMinimized ? "0" : "1";
lootTotals.style.padding = isLootListMinimized ? "0 10px" : "10px";
document.getElementById("lootToggleIcon").textContent =
isLootListMinimized ? "â–²" : "â–¼";
localStorage.setItem("lootListMinimized", isLootListMinimized);
};
const exportBtn = document.getElementById("lootExportBtn");
exportBtn.onclick = () => {
if (
!activePlayer ||
!playerLootData[activePlayer] ||
Object.keys(playerLootData[activePlayer]).length === 0
) {
alert("No loot data available for the active player to export.");
return;
}
try {
const dataToExport = playerLootData[activePlayer];
const csvContent = Object.entries(dataToExport)
.map(([hrid, count]) => {
let itemName = hrid.replace("/items/", "").replace(/_/g, " ");
itemName = `"${itemName.replace(/"/g, '""')}"`;
return `${itemName},${count}`;
})
.join("\n");
const csvOutput = "Item Name,Count\n" + csvContent;
navigator.clipboard
.writeText(csvOutput)
.then(() => {
const originalText = exportBtn.textContent;
exportBtn.textContent = "Copied!";
exportBtn.style.color = "#4caf50";
setTimeout(() => {
exportBtn.textContent = originalText;
exportBtn.style.color = "";
}, 1500);
})
.catch((err) => {
console.error(
"[LootTracker] Failed to copy CSV to clipboard:",
err
);
alert("Failed to copy CSV. See console.");
});
} catch (error) {
console.error("[LootTracker] Error generating CSV:", error);
alert("Error generating CSV data.");
}
};
document.getElementById("lootClearBtn").onclick = () => {
if (
confirm(
"Are you sure you want to clear ALL tracked loot data? This cannot be undone."
)
) {
clearAllLootData();
}
};
let dragging = false;
let offsetX = 0;
let offsetY = 0;
function beginDrag(e) {
if (e.target.closest("button")) return;
dragging = true;
panel.style.transition = "none";
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
document.body.style.userSelect = "none";
document.body.style.cursor = "move";
}
document
.getElementById("lootHeader")
.addEventListener("mousedown", beginDrag);
document
.getElementById("lootBottomDragger")
.addEventListener("mousedown", beginDrag);
document.addEventListener("mousemove", (e) => {
if (!dragging) return;
const newX = Math.max(
0,
Math.min(window.innerWidth - panel.offsetWidth, e.clientX - offsetX)
);
const newY = Math.max(
0,
Math.min(window.innerHeight - panel.offsetHeight, e.clientY - offsetY)
);
panel.style.left = `${newX}px`;
panel.style.top = `${newY}px`;
});
document.addEventListener("mouseup", () => {
if (!dragging) return;
dragging = false;
panel.style.transition = "";
document.body.style.userSelect = "";
document.body.style.cursor = "";
localStorage.setItem("lootOverlayTop", panel.style.top);
localStorage.setItem("lootOverlayLeft", panel.style.left);
});
console.log("[LootTracker] Overlay created.");
}
function updateLootDisplay(playerName) {
const container = document.getElementById("lootTotals");
const revenueLine = document.getElementById("lootRevenueLine");
if (!container) {
console.error(
"[LootTracker] updateLootDisplay: Could not find #lootTotals element!"
);
if (revenueLine) revenueLine.textContent = "Total Value: Error (UI)";
return;
}
if (!revenueLine) {
console.warn(
"[LootTracker] updateLootDisplay: Could not find #lootRevenueLine element."
);
}
if (!playerLootData[playerName]) {
container.innerHTML = "
• ${name} × ${count}${gainHTML} ${
!priceFound && !itemHrid.endsWith("/coin")
? '?'
: ""
}
`;
previousLootCounts[playerName][itemHrid] = count;
});
}
const hasNonCoinItems = sorted.some(([hrid]) => !hrid.endsWith("/coin"));
let finalRevenueText = "";
if (!marketDataAvailable && hasNonCoinItems && sorted.length > 0) {
finalRevenueText = `Total Value: Calculating...`;
} else if (sorted.length === 0) {
finalRevenueText = `Total Value: ${formatGold(0)}`;
} else {
finalRevenueText = `Total Value: ${formatGold(totalRevenue)}`;
}
try {
container.innerHTML = html;
if (revenueLine) {
revenueLine.textContent = finalRevenueText;
}
} catch (uiError) {
console.error(
`[LootTracker] CRITICAL: Error occurred during DOM update!`,
uiError
);
}
document.querySelectorAll(".fadeGain").forEach((el) => {
setTimeout(() => {
void el.offsetWidth;
el.style.opacity = "0";
}, 100);
});
if (
lastBattleLoot[playerName] &&
Object.keys(lastBattleLoot[playerName]).length > 0
) {
lastBattleLoot[playerName] = {};
}
}
function switchTab(playerName) {
activePlayer = playerName;
document.querySelectorAll("#lootTabs button").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.name === playerName);
});
updateLootDisplay(playerName);
}
function addTab(player) {
const playerName = player.name;
const lootMap = player.totalLootMap || {};
const container = document.getElementById("lootTabs");
if (!container) {
console.error("[LootTracker] Loot tabs container not found!");
return;
}
if (!playerLootData[playerName]) playerLootData[playerName] = {};
if (!previousLootCounts[playerName]) previousLootCounts[playerName] = {};
if (!lastBattleLoot[playerName]) lastBattleLoot[playerName] = {};
let tabNeedsUpdate = false;
for (const key in lootMap) {
const { itemHrid, count } = lootMap[key];
if (playerLootData[playerName][itemHrid] !== count) {
lastBattleLoot[playerName][itemHrid] =
playerLootData[playerName][itemHrid] || 0;
playerLootData[playerName][itemHrid] = count;
tabNeedsUpdate = true;
}
}
let tabButton = container.querySelector(
`button[data-name="${playerName}"]`
);
if (!tabButton) {
tabButton = document.createElement("button");
tabButton.textContent = playerName;
tabButton.dataset.name = playerName;
tabButton.onclick = () => switchTab(playerName);
container.appendChild(tabButton);
if (!activePlayer) activePlayer = playerName;
}
if (playerName === myPlayerName && !selfTabSelected) {
selfTabSelected = true;
switchTab(playerName);
tabNeedsUpdate = false;
} else if (playerName === activePlayer && tabNeedsUpdate) {
updateLootDisplay(playerName);
}
if (playerName === activePlayer) {
document.querySelectorAll("#lootTabs button").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.name === activePlayer);
});
}
}
function clearAllLootData() {
console.log("[LootTracker] Clearing all loot data.");
for (const p in playerLootData) {
playerLootData[p] = {};
previousLootCounts[p] = {};
lastBattleLoot[p] = {};
}
const tabsContainer = document.getElementById("lootTabs");
const totalsContainer = document.getElementById("lootTotals");
const revenueLine = document.getElementById("lootRevenueLine");
if (tabsContainer) tabsContainer.innerHTML = "";
if (totalsContainer)
totalsContainer.innerHTML = "