// ==UserScript== // @name MWI Nex Leaderboard // @namespace https://milkywayidle.com/ // @version 2.0 // @description Overhauls the leaderboard UI using data from li-mwi-leaderboard.ngrok.io // @match https://*.milkywayidle.com/* // @license MIT // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // @downloadURL https://update.greasyfork.icu/scripts/533027/MWI%20Nex%20Leaderboard.user.js // @updateURL https://update.greasyfork.icu/scripts/533027/MWI%20Nex%20Leaderboard.meta.js // ==/UserScript== (function () { "use strict"; // --- Configuration --- const SORT_LIST_WIDTH = "140px"; const COLLAPSED_STATE_KEY = "mwi_leaderboard_sort_collapsed"; // --- Global State --- let leaderboardData = null; let globalPlayerRanks = {}; let currentSortBy = "Total Level"; let isSortListCollapsed = GM_getValue(COLLAPSED_STATE_KEY, false); let isProcessing = false; const style = document.createElement("style"); style.textContent = ` /* Main Wrapper */ .mwi-wrapper { display: flex; height: 1000px; border: 1px solid #333; background: rgba(5, 5, 10, 0.85); color: #f0f0f0; font-family: 'Segoe UI', sans-serif; overflow: hidden; width: 98%; max-width: 1600px; margin: 5px auto; } /* Left Panel */ .mwi-left { width: 45%; min-width: 30px; border-right: 1px solid #222; display: flex; flex-direction: row; overflow: hidden; transition: width 0.3s ease; } /* Sort Options List */ .mwi-sort-options-list { width: ${SORT_LIST_WIDTH}; height: 100%; display: flex; flex-direction: column; background: rgba(10, 10, 20, 0.4); flex-shrink: 0; transition: width 0.3s ease, padding 0.3s ease, border 0.3s ease; overflow: hidden; } .mwi-sort-options-scroll { flex-grow: 1; overflow-y: auto; overflow-x: hidden; padding-bottom: 5px; } .mwi-sort-option { padding: 7px 10px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; color: #bbb; border-bottom: 1px solid #222; transition: background-color 0.2s, color 0.2s; } .mwi-sort-option:hover { background-color: rgba(255, 255, 255, 0.08); color: #fff; } .mwi-sort-option.active { background-color: rgba(60, 120, 180, 0.3); color: #7dd3fc; font-weight: bold; } /* Toggle Button */ .mwi-sort-toggle-btn { display: flex; align-items: center; justify-content: center; width: 20px; height: 100%; background-color: #18181f; border-right: 1px solid #2b2b33; color: #aaa; cursor: pointer; flex-shrink: 0; transition: background-color 0.2s, color 0.2s; font-size: 16px; font-weight: bold; line-height: 1; } .mwi-sort-toggle-btn:hover { background-color: #2a2a33; color: #eee; } /* Player List Area */ .mwi-player-area { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 150px; } .mwi-player-list-container { flex-grow: 1; overflow-y: auto; padding-right: 5px; } /* Player List Header */ .mwi-list-header { display: flex; justify-content: space-between; padding: 5px 4px; border-bottom: 1px solid #555; margin: 0 4px 4px 4px; font-weight: bold; color: #ddd; font-size: 11px; text-transform: uppercase; flex-shrink: 0; } .mwi-list-header > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Player List Columns */ .mwi-header-rank, .mwi-player-rank { flex-basis: 15%; color: #facc15; font-weight: bold; text-align: left; } .mwi-header-name, .mwi-player-name { flex-basis: 55%; padding: 0 5px; color: #f0f0f0; text-align: center; } .mwi-header-value, .mwi-player-value { flex-basis: 30%; text-align: center; color: #aaa; } .mwi-header-value { color: #ddd; } .mwi-header-rank { color: #ddd; } .mwi-header-name { color: #ddd; } /* Player Row */ .mwi-player-row { cursor: pointer; padding: 7px 4px; margin: 0 4px; border-bottom: 1px solid #222; transition: background 0.2s; display: flex; justify-content: space-between; align-items: center; font-size: 13px; } .mwi-player-row:hover { background: rgba(255, 255, 255, 0.05); } .mwi-player-row.selected { background: rgba(80, 100, 120, 0.2); } .mwi-player-row > span { flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; vertical-align: middle; } .mwi-player-name { line-height: normal; } /* --- Right Panel --- */ .mwi-right { width: 55%; padding: 0; background: rgba(10, 10, 20, 0.6); display: flex; flex-direction: column; overflow-y: hidden; } .mwi-right-content { flex-grow: 1; overflow-y: auto; padding: 5px; display: flex; flex-direction: column; } .mwi-right-content > em { margin: auto; font-size: 14px; color: #aaa; } .mwi-header { /* Player name header */ font-size: 20px; font-weight: bold; color: #ffe27a; margin-bottom: 7px; flex-shrink: 0; display: inline-block; line-height: normal; } .mwi-meta { /* Stats container */ font-size: 13px; line-height: 1.4; /* <<< Increased line height */ color: #bbb; margin-bottom: 8px; /* <<< Increased margin */ flex-shrink: 0; } .mwi-meta b { color: #ddd; } .mwi-meta-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px 20px; font-size: 13px; margin-top: 6px; align-items: start; } .mwi-meta-grid > div { white-space: nowrap; } .mwi-ranks-container { flex-shrink: 0; margin-top: 15px; /* <<< Increased margin */ } /* Right Panel Ranks Table Styling */ .mwi-rank-header-row { display: flex; width: 100%; align-items: center; padding: 6px 4px; /* <<< Increased padding */ border-bottom: 1px solid #666; font-weight: bold; color: #ddd; font-size: 11px; text-transform: uppercase; margin-bottom: 4px; /* <<< Increased margin */ } .mwi-rank-row { display: flex; width: 100%; align-items: center; padding: 5px 4px; /* <<< Increased padding */ border-bottom: 1px solid #2a2a33; font-size: 13px; line-height: 1.4; /* <<< Increased line height */ } .mwi-rank-row:last-child { border-bottom: none; } /* Ranks Table Columns */ .mwi-rh-skill, .mwi-rd-skill { flex-basis: 38%; text-align: left; padding-right: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .mwi-rh-level, .mwi-rd-level { flex-basis: 15%; text-align: center; } .mwi-rh-xphr, .mwi-rd-xphr { flex-basis: 22%; text-align: center; } .mwi-rh-rank, .mwi-rd-rank { flex-basis: 25%; text-align: right; padding-left: 10px; } /* Ranks Table Text Styles */ .mwi-rh-skill, .mwi-rh-level, .mwi-rh-xphr, .mwi-rh-rank { color: #ddd; } .mwi-rd-skill { color: #ccc; } .mwi-rd-level { color: #bbb; } .mwi-rd-xphr { color: #38bdf8; font-size: 11px; } .mwi-rd-rank { color: #facc15; font-weight: bold; } /* Collapsed State */ .mwi-left.collapsed .mwi-sort-options-list { width: 0; border-right: none; padding: 0; } .mwi-left.collapsed .mwi-sort-toggle-btn { border-right: 1px solid #333; } /* Media Query */ @media (min-width: 1900px) { .mwi-left { width: 40%; } .mwi-right { width: 60%; } .mwi-header-rank, .mwi-player-rank { flex-basis: 12%; } .mwi-header-name, .mwi-player-name { flex-basis: 63%; text-align: center; } .mwi-header-value, .mwi-player-value { flex-basis: 25%; } } `; document.head.appendChild(style); // --- Helper Functions --- function getSkillData(player, skillKey) { if (!player || !Array.isArray(player.skills)) { return { level: (skillKey === 'total_level' && player?.totalLevel) ? player.totalLevel : 0, exp: 0 }; } const skill = player.skills.find( (s) => s.skill.toLowerCase() === skillKey.toLowerCase() ); const level = skill?.level ?? ((skillKey === 'total_level' && player?.totalLevel) ? player.totalLevel : 0); const exp = skill?.exp ?? 0; return { level: (typeof level === 'number' && !isNaN(level)) ? level : 0, exp: (typeof exp === 'number' && !isNaN(exp)) ? exp : 0 }; } function formatNumber(n) { if (n === null || n === undefined || isNaN(n)) { return "0"; } const num = parseFloat(n); if (num === 0) return "0"; if (num >= 1e9) return (num / 1e9).toFixed(2) + "B"; if (num >= 1e6) return (num / 1e6).toFixed(2) + "M"; if (num >= 10000) return (num / 1e3).toFixed(1) + "K"; if (num >= 1000) return (num / 1e3).toFixed(0) + "K"; return String(Math.floor(num)); } // --- Rank Calculation Function --- function calculateAllPlayerRanks(players) { const calculatedRanks = {}; if (!players || players.length === 0) return calculatedRanks; players.forEach(p => { if(p?.name) calculatedRanks[p.name] = {}; }); const categories = [ "Total Level", "XP", "Combat Level", "House Level", "Enhancing", "Magic", "Crafting", "Milking", "Stamina", "Cooking", "Tailoring", "Brewing", "Cheesesmithing", "Intelligence", "Power", "Ranged", "Attack", "Defense", "Foraging", "Alchemy", "Woodcutting" ]; for (const category of categories) { let key = category.toLowerCase().replace(/ /g, "_"); const sortable = players.map(p => { if (!p || !p.name) return null; let sortValue = 0; if (category === "XP") { sortValue = p.totalXP || 0; } else if (category === "Combat Level") { sortValue = p.combatLevel || 0; } else if (category === "House Level") { sortValue = p.totalHouseLevel || 0; } else { sortValue = getSkillData(p, key).exp; } sortValue = (typeof sortValue === 'number' && !isNaN(sortValue)) ? sortValue : 0; return { name: p.name, value: sortValue }; }).filter(p => p !== null); sortable.sort((a, b) => b.value - a.value); let denseRank = 0; let lastValue = -Infinity; for (let i = 0; i < sortable.length; i++) { if (sortable[i].value !== lastValue) { denseRank++; lastValue = sortable[i].value; } if (calculatedRanks[sortable[i].name]) { calculatedRanks[sortable[i].name][key] = denseRank; } } } return calculatedRanks; } // --- Data Fetching --- async function fetchLeaderboardDataAndCalculateRanks() { if (isProcessing) { return; } isProcessing = true; try { const res = await fetch("https://li-mwi-leaderboard.ngrok.io/api/leaderboard"); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); } leaderboardData = await res.json(); if (!Array.isArray(leaderboardData)) { leaderboardData = []; } globalPlayerRanks = calculateAllPlayerRanks(leaderboardData); } catch (err) { console.error("[MWI] Failed to fetch or process leaderboard data:", err); leaderboardData = null; globalPlayerRanks = {}; } finally { isProcessing = false; } } // --- UI Injection and Rendering --- function injectCustomLeaderboard(container) { if (!leaderboardData) { container.innerHTML = "Error: Leaderboard data not available."; return; } container.innerHTML = ""; const wrapper = document.createElement("div"); wrapper.className = "mwi-wrapper"; const left = document.createElement("div"); left.className = "mwi-left"; if (isSortListCollapsed) { left.classList.add("collapsed"); } const sortOptionsList = document.createElement("div"); sortOptionsList.className = "mwi-sort-options-list"; const sortOptionsScroll = document.createElement("div"); sortOptionsScroll.className = "mwi-sort-options-scroll"; const sortOptions = [ "Total Level", "XP", "Combat Level", "House Level", "Enhancing", "Magic", "Crafting", "Milking", "Stamina", "Cooking", "Tailoring", "Brewing", "Cheesesmithing", "Intelligence", "Power", "Ranged", "Attack", "Defense", "Foraging", "Alchemy", "Woodcutting" ]; sortOptions.forEach((opt) => { const optionDiv = document.createElement("div"); optionDiv.className = "mwi-sort-option"; optionDiv.textContent = opt; optionDiv.dataset.sortBy = opt; if (opt === currentSortBy) { optionDiv.classList.add("active"); } sortOptionsScroll.appendChild(optionDiv); }); sortOptionsList.appendChild(sortOptionsScroll); const toggleBtn = document.createElement("div"); toggleBtn.className = "mwi-sort-toggle-btn"; toggleBtn.textContent = isSortListCollapsed ? "»" : "«"; toggleBtn.title = isSortListCollapsed ? "Expand Sort List" : "Collapse Sort List"; const playerArea = document.createElement("div"); playerArea.className = "mwi-player-area"; const listHeader = document.createElement("div"); listHeader.className = "mwi-list-header"; listHeader.innerHTML = `RANKNAMETYPE`; const headerValueSpan = listHeader.querySelector(".mwi-header-value"); const playerListContainer = document.createElement("div"); playerListContainer.className = "mwi-player-list-container"; playerArea.appendChild(listHeader); playerArea.appendChild(playerListContainer); left.appendChild(sortOptionsList); left.appendChild(toggleBtn); left.appendChild(playerArea); const right = document.createElement("div"); right.className = "mwi-right"; const rightContent = document.createElement("div"); rightContent.className = "mwi-right-content"; rightContent.innerHTML = "Select a player to view details"; right.appendChild(rightContent); function updateListHeader(sortByValue) { if (headerValueSpan) { headerValueSpan.textContent = sortByValue; headerValueSpan.title = `Sorted by: ${sortByValue}`; } } function renderPlayers(sortBy) { updateListHeader(sortBy); if (!leaderboardData) { playerListContainer.innerHTML = "