IdlePixel Market Overhaul

Overhaul of market UI and functionality.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         IdlePixel Market Overhaul
// @namespace    com.anwinity.idlepixel
// @version      1.0.17
// @description  Overhaul of market UI and functionality.
// @author       Anwinity
// @license      MIT
// @match        *://idle-pixel.com/login/play*
// @grant        none
// @require      https://greasyfork.org/scripts/441206-idlepixel/code/IdlePixel+.js?anticache=20220905
// ==/UserScript==

(function() {
    'use strict';

    const XP_PER = {
        stone: 0.1,
        copper: 1,
        iron: 5,
        silver: 10,
        gold: 20,
        promethium: 100,
        titanium: 300,

        bronze_bar: 5,
        iron_bar: 25,
        silver_bar: 50,
        gold_bar: 100,
        promethium_bar: 500,
        titanium_bar: 2000
    };

    const BONEMEAL_PER = {
        bones: 1,
        big_bones: 2,
        ice_bones: 3,
        ashes: 2
    };

    class MarketPlugin extends IdlePixelPlusPlugin {
        constructor() {
            super("market", {
                about: {
                    name: GM_info.script.name,
                    version: GM_info.script.version,
                    author: GM_info.script.author,
                    description: GM_info.script.description
                },
                config: [
                    {
                        id: "condensed",
                        label: "Condensed UI",
                        type: "boolean",
                        default: true
                    },
                    {
                        id: "sortMethod",
                        label: "Sort Method",
                        type: "select",
                        default: "default",
                        options: [
                            {value: "default", label: "Default"},
                            {value: "timeDESC", label: "Time (Newest First)"},
                            {value: "timeASC", label: "Time (Newest Last)"},
                            {value: "priceASC", label: "Price (Cheapest First)"},
                            {value: "priceDESC", label: "Price (Cheapest Last)"},
                        ]
                    },
                    {
                        id: "highlightBest",
                        label: "Highlight Best",
                        type: "boolean",
                        default: true
                    },
                    {
                        id: "autoMax",
                        label: "Autofill Max Buy",
                        type: "boolean",
                        default: false
                    },
                ]
            });
            this.lastBrowsedItem = "all";
            this.lastCategoryFilter = "all";
        }

        onConfigsChanged() {
            this.applyCondensed(this.getConfig("condensed"));
        }

        applyCondensed(condensed) {
            if(condensed) {
                $("#panel-player-market").addClass("condensed");
                $("#modal-market-select-item").addClass("condensed");
            }
            else {
                $("#panel-player-market").removeClass("condensed");
                $("#modal-market-select-item").removeClass("condensed");
            }
        }

        onLogin() {
            const self = this;

            $("head").append(`
            <style id="styles-market">

              #market-table {
                  margin-top: 0.5em !important;
              }
              #market-table tr.cheaper {
                  background-color: rgb(50, 205, 50, 0.25);
              }
              #market-table tr.cheaper > td {
                  background-color: rgb(50, 205, 50, 0.25);
              }
              #panel-player-market.condensed > center {
                  display: flex;
                  flex-direction: row;
                  justify-content: center;
              }
              #panel-player-market.condensed div.player-market-slot-base {
              	  height: 400px;
              }
              #panel-player-market.condensed div.player-market-slot-base hr {
                  margin-top: 2px;
                  margin-bottom: 4px;
              }
              #panel-player-market.condensed div.player-market-slot-base br + #panel-player-market.condensed div.player-market-slot-base br {
                  display: none;
              }
              #panel-player-market.condensed div.player-market-slot-base[id^="player-market-slot-occupied"] button {
                  padding: 2px;
              }

              #panel-player-market.condensed #market-table th {
              	padding: 2px 4px;
              }

              #panel-player-market.condensed #market-table td {
              	padding: 2px 4px;
              }

              #modal-market-select-item.condensed #modal-market-select-item-section .select-item-tradables-catagory {
                margin: 6px 6px;
                padding: 6px 6px;
              }

              #modal-market-select-item.condensed #modal-market-select-item-section .select-item-tradables-catagory hr {
                margin-top: 2px;
                margin-bottom: 2px;
              }
              #market-category-filters {
                display: flex;
                flex-direction: row;
                justify-content: center;
                align-items: start;
                flex-wrap: wrap;
                margin: 0.25em;
              }
              #market-category-filters > button {
                display: inline-block;
                margin: 0.25em;
              }
              #market-category-filters > button.active {
                background-color: #74DBDB;
              }

            </style>
            `);
            // modal-market-configure-item-to-sell-amount
            const sellModal = $("#modal-market-configure-item-to-sell");
            const sellAmountInput = sellModal.find("#modal-market-configure-item-to-sell-amount");
            sellAmountInput.after(`
              <button type="button" onclick="IdlePixelPlus.plugins.market.applyOneAmountSell()">1</button>
              <button type="button" onclick="IdlePixelPlus.plugins.market.applyMaxAmountSell()">max</button>
              <button type="button" onclick="IdlePixelPlus.plugins.market.applyMaxAmountSell(true)">max-1</button>
            `);
            const sellPriceInput = sellModal.find("#modal-market-configure-item-to-sell-price-each").after(`
              <button type="button" onclick="IdlePixelPlus.plugins.market.applyMinPriceSell()">min</button>
              <button type="button" onclick="IdlePixelPlus.plugins.market.applyMidPriceSell()">mid</button>
              <button type="button" onclick="IdlePixelPlus.plugins.market.applyMaxPriceSell()">max</button>
              <br /><br />
              Total: <span id="modal-market-configure-item-to-sell-total"></span>
            `);

            sellAmountInput.on("input change", () => this.applyTotalSell());
            sellPriceInput.on("input change", () => this.applyTotalSell());

            const buyModal = $("#modal-market-purchase-item");
            const buyAmountInput = buyModal.find("#modal-market-purchase-item-amount-input");
            buyAmountInput.after(`
              <button type="button" onclick="IdlePixelPlus.plugins.market.applyOneAmountBuy()">1</button>
              <button type="button" onclick="IdlePixelPlus.plugins.market.applyMaxAmountBuy()">max</button>
              <br /><br />
              Total: <span id="modal-market-purchase-item-total"></span>
              <br />
              Owned: <item-display data-format="number" data-key="coins"></item-display>
            `);
            buyAmountInput.on("input change", () => this.applyTotalBuy());

            // wrap Market.browse_get_table to capture last selected
            const original_market_browse = Market.browse_get_table;
            Market.browse_get_table = function(item) {
                return self.browseGetTable(item)
                    .always(() => {
                        self.filterTable();
                    });
            }

            $("#market-table").css("margin-top", "24px");
            $("#market-table").parent().before(`<div id="market-category-filters"><div>`);

            // wrap Market.load_tradables to populate category filters
            const original_load_tradables = Market.load_tradables;
            Market.load_tradables = function(data) {
                original_load_tradables.apply(this, arguments);
                self.createFilterButtons();
            }
            self.createFilterButtons();

            $(`#panel-player-market button[onclick^="Market.clicks_browse_player_market_button"]`)
                .first()
                .after(`<button id="refresh-market-table-button" type="button" style="margin-left: 0.5em" onclick="IdlePixelPlus.plugins.market.refreshMarket(true);">Refresh</button>`);

            //$("button.market-remove-button").after('<br><br><br><button onclick="" class="market-rebrowse-button">Browse</button>');

            this.onConfigsChanged();
        }

        browseGetTable(item) {
            //console.log(`browseGetTable("${item}")`);
            const self = this;
            this.lastBrowsedItem = item;
            if(item != "all") {
                self.lastCategoryFilter = "all";
            }
            if(item == "all") {
                $("#market-category-filters").show();
            }
            else {
                $("#market-category-filters").hide();
            }

            // A good chunk of this is taking directly from Market.browse_get_table
            hide_element("market-table");
            show_element("market-loading");
            let best = {};
            let bestList = {};
            return $.get(`../../market/browse/${item}/`).done(function(data) {
                const xpMultiplier = DonorShop.has_donor_active(IdlePixelPlus.getVar("donor_bonus_xp_timestamp")) ? 1.1 : 1;
                //console.log(data);
                data.forEach(datum => {
                    const priceAfterTax = datum.market_item_price_each * 1.01;
                    switch(datum.market_item_category) {
                        case "bars":
                        case "ores": {
                            let perCoin = (priceAfterTax / (xpMultiplier*XP_PER[datum.market_item_name]));
                            datum.perCoin = perCoin;
                            datum.perCoinLabel = `${perCoin.toFixed(perCoin < 10 ? 2 : 1)} coins/xp`;
                            if(!best[datum.market_item_category]) {
                                best[datum.market_item_category] = perCoin;
                                bestList[datum.market_item_category] = [datum];
                            }
                            else {
                                if(perCoin == best[datum.market_item_category]) {
                                    bestList[datum.market_item_category].push(datum);
                                }
                                else if(perCoin < best[datum.market_item_category]) {
                                    bestList[datum.market_item_category] = [datum];
                                    best[datum.market_item_category] = perCoin;
                                }
                            }
                            break;
                        }
                        case "logs": {
                            let perCoin = (priceAfterTax / Cooking.getHeatPerLog(datum.market_item_name));
                            datum.perCoin = perCoin;
                            datum.perCoinLabel = `${perCoin.toFixed(perCoin < 10 ? 2 : 1)} coins/heat`;
                            if(!best[datum.market_item_category]) {
                                best[datum.market_item_category] = perCoin;
                                bestList[datum.market_item_category] = [datum];
                            }
                            else {
                                if(perCoin == best[datum.market_item_category]) {
                                    bestList[datum.market_item_category].push(datum);
                                }
                                else if(perCoin < best[datum.market_item_category]) {
                                    bestList[datum.market_item_category] = [datum];
                                    best[datum.market_item_category] = perCoin;
                                }
                            }
                            break;
                        }
                        case "raw_fish":
                        case "cooked_fish":{
                            let perCoin = (priceAfterTax / Cooking.get_energy(datum.market_item_name));
                            datum.perCoin = perCoin;
                            datum.perCoinLabel = `${perCoin.toFixed(perCoin < 10 ? 2 : 1)} coins/energy`;
                            if(!best[datum.market_item_category]) {
                                best[datum.market_item_category] = perCoin;
                                bestList[datum.market_item_category] = [datum];
                            }
                            else {
                                if(perCoin == best[datum.market_item_category]) {
                                    bestList[datum.market_item_category].push(datum);
                                }
                                else if(perCoin < best[datum.market_item_category]) {
                                    bestList[datum.market_item_category] = [datum];
                                    best[datum.market_item_category] = perCoin;
                                }
                            }
                            break;
                        }
                        case "bones": {
                            let perCoin = (priceAfterTax / BONEMEAL_PER[datum.market_item_name]);
                            datum.perCoin = perCoin;
                            datum.perCoinLabel = `${perCoin.toFixed(perCoin < 10 ? 2 : 1)} coins/bonemeal`;
                            if(!best[datum.market_item_category]) {
                                best[datum.market_item_category] = perCoin;
                                bestList[datum.market_item_category] = [datum];
                            }
                            else {
                                if(perCoin == best[datum.market_item_category]) {
                                    bestList[datum.market_item_category].push(datum);
                                }
                                else if(perCoin < best[datum.market_item_category]) {
                                    bestList[datum.market_item_category] = [datum];
                                    best[datum.market_item_category] = perCoin;
                                }
                            }
                            break;
                        }
                        default: {
                            datum.perCoin = Number.MAX_SAFE_INTEGER;
                            datum.perCoinLabel = "";
                            break;
                        }
                    }
                });
                Object.values(bestList).forEach(bestCatList => bestCatList.forEach(datum => datum.best=true));
                const sortMethod = self.getConfig("sortMethod");
                switch(sortMethod) {
                    case "timeDESC": {
                        data = data.sort((a, b) => b.market_item_post_timestamp - a.market_item_post_timestamp);
                        break;
                    }
                    case "timeASC": {
                        data = data.sort((a, b) => a.market_item_post_timestamp - b.market_item_post_timestamp);
                        break;
                    }
                    case "priceASC": {
                        data = data.sort((a, b) => {
                            if(a.perCoin != b.perCoin && typeof a.perCoin==="number" && typeof b.perCoin==="number") {
                                return a.perCoin - b.perCoin;
                            }
                            return a.market_item_price_each - b.market_item_price_each;
                        });

                        // DEBUG
                        //data.filter(x => x.market_item_category == "cooked_fish").forEach(d => {
                        //    console.log(`${d.market_item_name} ${d.perCoin} ${d.market_item_price_each}`);
                        //});
                        //

                        break;
                    }
                    case "priceDESC": {
                        data = data.sort((a, b) => {
                            if(a.perCoin != b.perCoin && typeof a.perCoin==="number" && typeof b.perCoin==="number") {
                                return b.perCoin - a.perCoin;
                            }
                            return b.market_item_price_each - a.market_item_price_each;
                        });
                        break;
                    }
                }
                // console.log(data);
                let html = "<tr><th>ITEM</th><th></th><th>AMOUNT</th><th>PRICE EACH</th><th>CATEGORY</th><th>EXPIRES IN</th></tr>";
                // in case you want to add any extra data to the table but still use this script
                if(typeof window.ModifyMarketDataHeader === "function") {
                    html = window.ModifyMarketDataHeader(html);
                }
                data.forEach(datum => {
                    let market_id = datum.market_id;
                    let player_id = datum.player_id;
                    let item_name = datum.market_item_name;
                    let amount = datum.market_item_amount;
                    let price_each = datum.market_item_price_each;
                    let category = datum.market_item_category;
                    let timestamp = datum.market_item_post_timestamp;
                    let perCoinLabel = datum.perCoinLabel;
                    let best = datum.best && self.getConfig("highlightBest");

                    let your_entry = "";
                    if(Items.getItem("player_id") == player_id) {
                        your_entry = "<span class='color-grey font-small'><br /><br />(Your Item)</span>";
                    }

                    let rowHtml = "";
                    rowHtml += `<tr onclick="Modals.market_purchase_item('${market_id}', '${item_name}', '${amount}', '${price_each}'); IdlePixelPlus.plugins.market.applyMaxAmountBuyIfConfigured();" class="hover${ best ? ' cheaper' : '' }">`;
                    rowHtml += `<td>${Items.get_pretty_item_name(item_name)}${your_entry}</td>`;
                    rowHtml += `<td><img src="https://d1xsc8x7nc5q8t.cloudfront.net/images/${item_name}.png" /></td>`;
                    rowHtml += `<td>${amount}</td>`;
                    rowHtml += `<td><img src="https://d1xsc8x7nc5q8t.cloudfront.net/images/coins.png" /> ${Market.get_price_after_tax(price_each)}`;
                    if(perCoinLabel) {
                        rowHtml += `<br /><span style="font-size: 80%; opacity: 0.8">${perCoinLabel}</span>`;
                    }
                    rowHtml += `</td>`;
                    rowHtml += `<td>${category}</td>`;
                    rowHtml += `<td>${Market._get_expire_time(timestamp)}</td>`;
                    rowHtml += `</tr>`;

                    // in case you want to add any extra data to the table but still use this script
                    if(typeof window.ModifyMarketDataRow === "function") {
                        rowHtml = window.ModifyMarketDataRow(datum, rowHtml);
                    }
                    html += rowHtml;
                });

                document.getElementById("market-table").innerHTML = html;
                hide_element("market-loading");
                show_element("market-table");
            });

        }

        createFilterButtons() {
            const filters = $("#market-category-filters");
            filters.empty();
            if(Market.tradables) {
                const categories = [];
                Market.tradables.forEach(tradable => {
                    if(!categories.includes(tradable.category)) {
                        categories.push(tradable.category);
                    }
                });
                filters.append(`<button data-category="all" onclick="IdlePixelPlus.plugins.market.filterTable('all')">All</button>`);
                categories.forEach(cat => {
                    filters.append(`<button data-category="${cat}" onclick="IdlePixelPlus.plugins.market.filterTable('${cat}')">${cat.replace(/_/g, " ").replace(/(^|\s)\w/g, s => s.toUpperCase())}</button>`);
                });
            }
        }

        filterTable(category) {

            if(category) {
                this.lastCategoryFilter = category;
            }
            else {
                category = this.lastCategoryFilter || "all";
            }

            $("#market-category-filters button.active").removeClass("active");
            $(`#market-category-filters button[data-category="${category}"]`).addClass("active");

            const rows = $("#market-table tbody tr.hover");
            if(category=="all") {
                rows.show();
            }
            else {
                rows.each(function() {
                    const row = $(this);
                    const rowCategory = row.find("td:nth-child(5)").text();
                    if(category == rowCategory) {
                        row.show();
                    }
                    else {
                        row.hide();
                    }
                });

            }
        }

        refreshMarket(disableButtonForABit) {
            if(this.lastBrowsedItem) {
                Market.browse_get_table(this.lastBrowsedItem);
                if(disableButtonForABit) { // prevent spam clicking it
                    $("#refresh-market-table-button").prop("disabled", true);
                    setTimeout(() => {
                        $("#refresh-market-table-button").prop("disabled", false);
                    }, 700);
                }
            }
        }

        applyOneAmountBuy() {
            $("#modal-market-purchase-item #modal-market-purchase-item-amount-input").val(1);
            this.applyTotalBuy();
        }

        applyMaxAmountBuyIfConfigured() {
            if(this.getConfig("autoMax")) {
                this.applyMaxAmountBuy();
            }
        }

        applyMaxAmountBuy(minus1=false) {
            const coinsOwned = IdlePixelPlus.getVarOrDefault("coins", 0, "int");
            const price = parseInt($("#modal-market-purchase-item #modal-market-purchase-item-price-each").val().replace(/[^\d]+/g, ""));
            const maxAffordable = Math.floor(coinsOwned / price);
            const maxAvailable = parseInt($("#modal-market-purchase-item #modal-market-purchase-item-amount-left").val().replace(/[^\d]+/g, ""));
            let max = Math.min(maxAffordable, maxAvailable);
            if(minus1) {
                max--;
            }
            if(max < 0) {
                max = 0;
            }
            $("#modal-market-purchase-item #modal-market-purchase-item-amount-input").val(max);
            this.applyTotalBuy();
        }

        parseIntKMBT(s) {
            if(typeof s === "number") {
                return Math.floor(s);
            }
            s = s.toUpperCase().replace(/[^\dKMBT]+/, "");
            if(s.endsWith("K")) {
                s = s.replace(/K$/, "000");
            }
            else if(s.endsWith("M")) {
                s = s.replace(/M$/, "000000");
            }
            else if(s.endsWith("B")) {
                s = s.replace(/B$/, "000000000");
            }
            else if(s.endsWith("T")) {
                s = s.replace(/T$/, "000000000000");
            }
            return parseInt(s);
        }

        applyTotalBuy() {
            const amount = this.parseIntKMBT($("#modal-market-purchase-item #modal-market-purchase-item-amount-input").val());
            const price = this.parseIntKMBT($("#modal-market-purchase-item #modal-market-purchase-item-price-each").val().replace("Price each: ", ""));
            const total = amount*price;
            const totalElement = $("#modal-market-purchase-item-total");
            if(isNaN(total)) {
                totalElement.text("");
            }
            else {
                totalElement.text(total.toLocaleString());
                const coinsOwned = IdlePixelPlus.getVarOrDefault("coins", 0, "int");
                if(total > coinsOwned) {
                    totalElement.css("color", "red");
                }
                else {
                    totalElement.css("color", "");
                }
            }
        }

        currentItemSell() {
            return $("#modal-market-configure-item-to-sell").val();
        }

        applyOneAmountSell() {
            const item = this.currentItemSell();
            const owned = IdlePixelPlus.getVarOrDefault(item, 0, "int");
            $("#modal-market-configure-item-to-sell-amount").val(Math.min(owned, 1));
            this.applyTotalSell();
        }

        applyMaxAmountSell(minus1=false) {
            const item = this.currentItemSell();
            let max = IdlePixelPlus.getVarOrDefault(item, 0, "int");
            if(minus1) {
                max--;
            }
            if(max < 0) {
                max = 0;
            }
            $("#modal-market-configure-item-to-sell-amount").val(max);
            this.applyTotalSell();
        }

        applyMinPriceSell() {
            const min = parseInt($("#modal-market-configure-item-to-sell-label-lower-limit").text().replace(/[^\d]/g, ""));
            $("#modal-market-configure-item-to-sell-price-each").val(min);
            this.applyTotalSell();
        }

        applyMidPriceSell() {
            const min = parseInt($("#modal-market-configure-item-to-sell-label-lower-limit").text().replace(/[^\d]/g, ""));
            const max = parseInt($("#modal-market-configure-item-to-sell-label-upper-limit").text().replace(/[^\d]/g, ""));
            const mid = Math.floor((min+max)/2);
            $("#modal-market-configure-item-to-sell-price-each").val(mid);
            this.applyTotalSell();
        }

        applyMaxPriceSell() {
            const max = parseInt($("#modal-market-configure-item-to-sell-label-upper-limit").text().replace(/[^\d]/g, ""));
            $("#modal-market-configure-item-to-sell-price-each").val(max);
            this.applyTotalSell();
        }

        applyTotalSell() {
            const amount = this.parseIntKMBT($("#modal-market-configure-item-to-sell-amount").val());
            const price = this.parseIntKMBT($("#modal-market-configure-item-to-sell-price-each").val());
            const total = amount*price;
            if(isNaN(total)) {
                $("#modal-market-configure-item-to-sell-total").text("");
            }
            else {
                $("#modal-market-configure-item-to-sell-total").text(total.toLocaleString());
            }
            // TODO total w/ tax
        }

        onVariableSet(key, valueBefore, valueAfter) {

        }

    }

    const plugin = new MarketPlugin();
    IdlePixelPlus.registerPlugin(plugin);

})();