// ==UserScript== // @name MouseHunt - Marketplace UI Tweaks // @author Tran Situ (tsitu) // @namespace https://greasyfork.org/en/users/232363-tsitu // @version 1.4 // @description Adds useful features and tweaks to the Marketplace rework // @match http://www.mousehuntgame.com/* // @match https://www.mousehuntgame.com/* // @downloadURL none // ==/UserScript== (function() { /** * [ Notes ] * innerText has poor retrieval perf, use textContent * http://perfectionkills.com/the-poor-misunderstood-innerText/ * Is there a better way to center scrollRow vertically within table? * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView */ MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; // Initialize 'Browse' tab item caching if (localStorage.getItem("marketplace-browse-cache-tsitu") === null) { const cacheObj = { Cheese: 0, "Baskets & Kits": 0, Charms: 0, Crafting: 0, Special: 0, Collectibles: 0, Weapons: 0, Skins: 0 }; localStorage.setItem( "marketplace-browse-cache-tsitu", JSON.stringify(cacheObj) ); } // Only observe changes to the #overlayPopup element const observerTarget = document.querySelector("#overlayPopup"); const observer = new MutationObserver(function() { // TODO: Change some of the document.qS calls to a more specific element in case of collision // Check if the Marketplace interface is open if (document.querySelector(".marketplaceView")) { // Disconnect and reconnect later to prevent mutation loop observer.disconnect(); // Feature: Move close button to top right and clean up visuals const oldClose = document.querySelector(".button[type=submit]"); if (oldClose) { const newClose = oldClose.cloneNode(); oldClose.remove(); const suffix = document.querySelector(".suffix"); if (suffix) suffix.remove(); newClose.style.position = "absolute"; newClose.style.right = "0px"; newClose.style.top = "5px"; document.querySelector(".marketplaceView").prepend(newClose); const searchContainer = document.querySelector( ".marketplaceView-header-searchContainer" ); if (searchContainer) { searchContainer.style.right = "65px"; searchContainer.style.width = "220px"; } const searchBar = document.querySelector( ".marketplaceView-header-search" ); if (searchBar) { searchBar.style.width = "184px"; } // Remove 'X' in top right const topX = document.querySelector("#jsDialogClose"); if (topX) topX.remove(); } const browseTab = document.querySelector("[data-tab=browse].active"); const backButton = document.querySelector("a.marketplaceView-breadcrumb"); if (browseTab && !backButton) { /* Browse tab logic (active Browse tab + inactive 'Back' button) */ // Align trend icon divs to the right const trendIcons = document.querySelectorAll( ".marketplaceView-trendIcon" ); trendIcons.forEach(el => { const td = el.parentElement; if (td) { td.style.textAlign = "right"; } }); const sidebar = document.querySelector( ".marketplaceView-browse-sidebar" ); const itemType = sidebar.querySelector( ".marketplaceView-browse-sidebar-link.active" ); // Feature: Make item images 40x40 px document.querySelectorAll(".marketplaceView-itemImage").forEach(el => { el.style.width = "40px"; el.style.height = "40px"; el.style.backgroundSize = "100%"; el.style.minHeight = "40px"; }); let totalValueSum = 0; /** * Abbreviates large number values up to 1 decimal point * k = 1,000 and m = 1,000,000 * @param {number} num Integer to abbreviate * @return {string} */ function abbrev(num) { if (num <= 999) { return "" + num; } else if (num >= 1000 && num <= 999999) { let pre = Math.floor(num / 1000); let post = Math.round((num % 1000) / 100); if (post === 10) { post = 0; pre += 1; } return `${pre}.${post}k`; } else if (num >= 1000000) { let pre = Math.floor(num / 1000000); let post = Math.round((num % 1000000) / 100000); if (post === 10) { post = 0; pre += 1; } return `${pre}.${post}m`; } } const rows = document.querySelectorAll("tr[data-item-id]"); if (rows.length > 0) { const avgPriceHeader = document.querySelector( "th.marketplaceView-table-averagePrice" ); const valueHeader = document.createElement("th"); valueHeader.innerText = "Value"; valueHeader.className = "marketplaceView-table-estvalue marketplaceView-table-numeric"; // TODO: Implement Value column sort (will probably touch many hg.* functions) // "marketplaceView-table-estvalue marketplaceView-table-numeric sortable"; // valueHeader.setAttribute("data-property", "value"); // valueHeader.onclick = function() { // hg.views.MarketplaceView.setSortOrder(this); // return false; // }; if ( avgPriceHeader && !document.querySelector(".marketplaceView-table-estvalue") ) { // Add 'Value' column header avgPriceHeader.insertAdjacentElement("afterend", valueHeader); rows.forEach(row => { // Add click handlers to the 's that open up an item page row.querySelectorAll("a").forEach(el => { const aText = el.onclick; if (aText) { if (aText.toString().indexOf("showItem") >= 0) { el.addEventListener("click", function() { // Parse current item name and type for caching const name = row.querySelector( ".marketplaceView-table-name" ); if (name && itemType) { // Retrieve and overwrite localStorage const lsText = localStorage.getItem( "marketplace-browse-cache-tsitu" ); if (lsText) { const lsObj = JSON.parse(lsText); lsObj[itemType.textContent] = name.textContent; localStorage.setItem( "marketplace-browse-cache-tsitu", JSON.stringify(lsObj) ); } } }); } } }); // Parse owned quantity let ownedNum = 0; const ownedText = row.querySelector( ".marketplaceView-table-quantity" ).textContent; if (ownedText !== "-") { ownedNum = parseInt(ownedText.split(",").join("")); } // Parse average prices let priceNum = 0; const priceText = row.querySelector(".marketplaceView-goldValue"); if (priceText.children.length > 0) { priceNum = parseInt( priceText.children[0].title .split(" ")[0] .split(",") .join("") ); } const multValue = ownedNum * priceNum; if (multValue > 0) { totalValueSum += multValue; row.title = `${multValue.toLocaleString()} Gold`; } let outputText = abbrev(multValue); if (priceNum === 0) { // Avg. Price currently unavailable, but value isn't necessarily 0 outputText = "N/A"; } const valueColumn = document.createElement("td"); valueColumn.innerText = outputText; valueColumn.className = "marketplaceView-table-numeric value-column-tsitu"; // Feature: Insert 'Own' x 'Avg. Price' = 'Value' column data row .querySelector(".marketplaceView-table-averagePrice") .insertAdjacentElement("afterend", valueColumn); }); } } // Add info to the sidebar if (sidebar && !document.querySelector(".marketplace-sidebar-tsitu")) { // Container div const div = document.createElement("div"); div.className = "marketplace-sidebar-tsitu"; div.style.margin = "20px"; // Highlighted text const span1 = document.createElement("span"); span1.style.backgroundColor = "#D6EBA1"; span1.innerText = "highlighted green"; // Other text const span2 = document.createElement("span"); span2.innerText = "Last viewed item is "; div.appendChild(span2); div.appendChild(span1); // Feature: Add with sum of values on current tab const filterDiv = document.querySelector( ".marketplaceView-browse-filterContainer" ); if ( filterDiv && !document.querySelector(".marketplace-total-value-tsitu") ) { const span = document.createElement("span"); span.className = "marketplace-total-value-tsitu"; span.innerText = `Total estimated value on this tab: ${totalValueSum.toLocaleString()} Gold`; div.appendChild(document.createElement("br")); div.appendChild(document.createElement("br")); div.appendChild(span); } // Inject into DOM sidebar.appendChild(div); } // Feature: Check cache for most recently clicked item and scroll to it const lsText = localStorage.getItem("marketplace-browse-cache-tsitu"); if (lsText) { const lsObj = JSON.parse(lsText); const itemType = sidebar.querySelector( ".marketplaceView-browse-sidebar-link.active" ); if (itemType) { const name = lsObj[itemType.textContent]; if (name && name !== 0) { /** * Return row element that matches existing cached item name * @param {string} name Cached item name * @return {HTMLElement|false} that should be highlighted and scrolled to */ function findElement(name) { for (let el of document.querySelectorAll("tr[data-item-id]")) { const aTags = el.querySelectorAll("a"); if (aTags.length === 5) { if (name === aTags[2].textContent) { return el; } } } return false; } // Calculate index for nth-child const targetEl = findElement(name); let nthChildValue = 0; for (let i = 0; i < rows.length; i++) { const el = rows[i]; if (el === targetEl) { nthChildValue = i + 2; break; } } // tr:nth-child value (min = 2) const recentRow = document.querySelector( `.marketplaceView-table tr:nth-child(${nthChildValue})` ); if (recentRow) { recentRow.style.backgroundColor = "#D6EBA1"; // Try scrolling up to 4 rows down let scrollRow = recentRow; for (let i = 4; i > 0; i--) { const row = document.querySelector( `.marketplaceView-table tr:nth-child(${nthChildValue + i})` ); if (row) { scrollRow = row; break; } } scrollRow.scrollIntoView({ // Seems to wait for XHR & render - slow initially but gets moderately faster behavior: "auto", block: "nearest", inline: "nearest" }); } } } } } else if (backButton) { /* Listing logic (active 'Back' button) */ // Feature: Inject tariff info into Sell & Buy Orders rows const sellOrders = document.querySelector( ".marketplaceView-item-quickListings.sell" ); const buyOrders = document.querySelector( ".marketplaceView-item-quickListings.buy" ); if (sellOrders && buyOrders) { const goldValues = document.querySelectorAll( "td .marketplaceView-goldValue:not(.marketplaceView-suggestedPrice):not(.tsitu-link-bo-minus):not(.tsitu-link-bo-plus)" ); if (goldValues.length > 0) { goldValues.forEach(el => { const rawVal = el.textContent; if (typeof rawVal === "string") { const value = parseInt(rawVal.split(",").join("")); const tax = Math.ceil(value / 11); const preTax = value - tax; const titleString = `Raw: ${preTax.toLocaleString()}\nTax: ${tax.toLocaleString()}`; const ppEl = el.parentElement.parentElement; if (ppEl.nodeName === "TR") ppEl.title = titleString; const qtyEl = ppEl.querySelector( ".marketplaceView-table-listing-quantity" ); // Add a reversible onclick with same info as title if (qtyEl && !qtyEl.onclick) { const qtyVal = qtyEl.textContent; function initHandler() { qtyEl.innerText = qtyVal + "\n" + titleString; qtyEl.onclick = revertHandler; } function revertHandler() { qtyEl.innerText = qtyVal; qtyEl.onclick = initHandler; } qtyEl.onclick = initHandler; } } }); } } // Feature: More Quick Links const orderButton = document.querySelector( ".mousehuntActionButton.marketplaceView-item-submitButton" ); if (orderButton) { // Price suggestion parent divs const qtySuggest = document.querySelector( ".marketplaceView-item-input.quantity .marketplaceView-item-input-suggested" ); const priceSuggest = document.querySelector( ".marketplaceView-item-input.unitPrice .marketplaceView-item-input-suggested" ); const txType = document.querySelector( ".marketplaceView-item-actionType .marketplaceView-listingType" ).classList[1]; // Check existence of qtySuggest b/c directly clicking 'Sell' results in separate mutations if (txType === "sell" && qtySuggest) { // Existing 'Sell All' link to clone const sellAll = qtySuggest.children[0]; // Check custom class name to prevent multiple appends if (sellAll && !document.querySelector(".tsitu-link-sabo")) { const saQty = parseInt( sellAll.textContent .split(": ")[1] .split(",") .join("") ); if (saQty > 1) { const sellAllButOne = sellAll.cloneNode(); sellAllButOne.className = "tsitu-link-sabo"; sellAllButOne.setAttribute( "onclick", `hg.views.MarketplaceView.setOrderQuantity(${saQty - 1}); return false;` ); sellAllButOne.innerText = `[ Sell All But One: ${( saQty - 1 ).toLocaleString()} ]`; qtySuggest.appendChild(document.createElement("br")); qtySuggest.appendChild(document.createElement("br")); qtySuggest.appendChild(sellAllButOne); } } const firstRow = sellOrders.querySelector( "td .marketplaceView-goldValue" ); if (firstRow) { const rawVal = firstRow.textContent; if (typeof rawVal === "string") { const value = parseInt(rawVal.split(",").join("")); let offerValue = Math.round(value * 0.9999); if (offerValue >= value) offerValue = value - 1; // Minimum increment if (offerValue <= 10) offerValue = undefined; // Must be at least 10 Gold const bestSell = document.querySelector( ".sell > .marketplaceView-bestPrice" ); if (bestSell.textContent.length > 6) { bestSell.innerText = bestSell.textContent.replace( "Best", "Quick Sell" ); } // Generate manually, not guaranteed an existing link if (offerValue) { const boMinusLink = document.createElement("a"); boMinusLink.href = "#"; boMinusLink.setAttribute( "onclick", `hg.views.MarketplaceView.setOrderPrice(${offerValue}); return false;` ); boMinusLink.className = "marketplaceView-goldValue tsitu-link-bo-minus"; boMinusLink.innerText = `[ Undercut - 0.01%: ${offerValue.toLocaleString()} ]`; if (!document.querySelector(".tsitu-link-bo-minus")) { if (bestSell) { bestSell.insertAdjacentElement("afterend", boMinusLink); } } } } } } else if (txType === "buy") { const firstRow = buyOrders.querySelector( "td .marketplaceView-goldValue" ); if (firstRow) { const rawVal = firstRow.textContent; if (typeof rawVal === "string") { const value = parseInt(rawVal.split(",").join("")); let offerValue = Math.round(value * 1.0001); if (offerValue <= value) offerValue = value + 1; // Minimum increment if (offerValue >= 4294967293) offerValue = undefined; // Maximum tx amount const bestBuy = document.querySelector( ".buy > .marketplaceView-bestPrice" ); if (bestBuy.textContent.length > 6) { bestBuy.innerText = bestBuy.textContent.replace( "Best", "Quick Buy" ); } // Generate manually, not guaranteed an existing link if (offerValue) { const boPlusLink = document.createElement("a"); boPlusLink.href = "#"; boPlusLink.setAttribute( "onclick", `hg.views.MarketplaceView.setOrderPrice(${offerValue}); return false;` ); boPlusLink.className = "marketplaceView-goldValue tsitu-link-bo-plus"; boPlusLink.innerText = `[ Overbid + 0.01%: ${offerValue.toLocaleString()} ]`; if (!document.querySelector(".tsitu-link-bo-plus")) { if (bestBuy) { bestBuy.insertAdjacentElement("afterend", boPlusLink); } } } } } } } // 'Back' scrolls too far but clicking 'Browse' tab works fine // Additional scrolling logic in showItemInBrowser() is to blame // Solution: Override 'Back' button behavior only on the 'Browse' tab if (browseTab) { backButton.setAttribute( "onclick", "hg.views.MarketplaceView.showBrowser(); return false;" ); } } // Re-observe after mutation-inducing logic observer.observe(observerTarget, { childList: true, subtree: true }); } }); // Initial observe observer.observe(observerTarget, { childList: true, subtree: true }); })();