Subeta Sorting

This script sorts NPC shops prices by profit.

// ==UserScript==
// @name         Subeta Sorting
// @namespace    http://artees.pw/
// @description  This script sorts NPC shops prices by profit.
// @version      1.8
// @author       Artees
// @match        *://subeta.net/shop.php?shop*
// @require      https://greasyfork.org/scripts/30529-subeta-searching/code/Subeta%20Searching.js?version=203734
// @grant        none
// @icon         https://subeta.net/favicon.ico
// @icon64       https://img.subeta.net/items/bottle_apothecary_06.gif
// ==/UserScript==

const KEY_MIN_PROFIT = "minProfit",
    KEY_MAX_ITEMS = "maxItems",
    KEY_BUYING = "buying",
    KEY_SHOP_URL = "shopUrl";

var curItem = parseItemsForSorting(),
    sortedItems = [],
    minProfit = getFromStorage(localStorage, KEY_MIN_PROFIT, 0),
    maxItems = getFromStorage(localStorage, KEY_MAX_ITEMS, 10),
    buyCountdown = null,
    isBuying = getFromStorage(localStorage, KEY_BUYING, false),
    buyingIntervalId = 0,
    buyingTimeoutId = 0,
    isSortingComplete = false,
    pricedCount = 0;

function parseItemsForSorting() {
    var itemDivs = document.getElementsByClassName("two wide column");
    if (itemDivs.length === 0 || itemDivs[0].getElementsByTagName("form").length === 0) return null;
    function getPriceField(itemDiv) {
        return new PriceField(itemDiv);
    }
    return parseItems(itemDivs, getPriceField);
}

function Item(itemDiv, priceField) {
    var start = itemDiv.innerHTML.indexOf("<br>"),
        end = itemDiv.innerHTML.indexOf("<br>", start + 4);
    this.itemDiv = itemDiv;
    this.name = replaceAll(itemDiv.innerHTML.substring(start + 4, end), "	", "");
    this.name = replaceAll(this.name, "\n", "");
    this.cacheKey = "c " + this.name;
    this.next = null;
    this.isPriced = false;
    this.priceField = priceField;
    this.resetPrice = function () {
        var end = this.priceField.value.indexOf(CY) + CY.length;
        return this.priceField.value.substring(0, end);
    };
    this.priceField.value = this.resetPrice();
    this.getPrice = function () {
        var priceString = replaceAll(this.resetPrice(), ",", "");
        return parseInt(priceString);
    };
    this.actualPrice = this.getPrice();
    var isComplete = true;
    this.onSelect = function() {
        isComplete = true;
        forEachItem(getNext);
        if (isComplete) {
            isSortingComplete = true;
            switchBuying();
        }
        return !isComplete;
    };
    function getNext(i) {
        if (i === null || !i.isPriced) {
            curItem = i;
            isComplete = false;
            return false;
        }
        return true;
    }
}

function PriceField(itemDiv) {
    var label = itemDiv.getElementsByTagName("b")[0];
    return {
        get value() {
            return label.innerText;
        },
        set value(value) {
            label.innerText = value;
        },
        get style() {
            return label.style;
        },
        set style(value) {
            label.style = value;
        }
    };
}

createUI();

function createUI() {
    var s = document.getElementsByClassName("shop_item_container")[0];
    if (s === undefined) {
        goBackAfterBuying();
        return;
    }
    var ui = s.insertBefore(document.createElement("div"), s.firstChild);
    ui.appendChild(document.createTextNode("Delay: "));
    var itemDelayInput = document.createElement("INPUT");
    itemDelayInput.value = itemDelay + " sec.";
    itemDelayInput.onchange = function () {
        itemDelay = parseInt(this.value);
        localStorage[KEY_ITEM_DELAY] = itemDelay;
    };
    ui.appendChild(itemDelayInput);
    ui.appendChild(createSpace());
    var button = document.createElement("a");
    button.className = "ui button tiny";
    button.innerText = "Purge Cache";
    button.onclick = function () {
        forEachItem(function (i) {
            localStorage.removeItem(i.cacheKey);
            location.reload();
            return true;
        });
    };
    ui.appendChild(button);
    ui.appendChild(document.createElement("br"));
    var autobuy = document.createElement("input");
    autobuy.type = "checkbox";
    autobuy.checked = isBuying;
    autobuy.onchange = function () {
        isBuying = this.checked;
        localStorage[KEY_BUYING] = isBuying;
        switchBuying();
    };
    ui.appendChild(autobuy);
    ui.appendChild(document.createTextNode(" Autobuy"));
    ui.appendChild(createSpace());
    ui.appendChild(document.createTextNode("Min profit: "));
    var minProfitInput = document.createElement("INPUT");
    minProfitInput.value = minProfit + CY;
    minProfitInput.onchange = function () {
        minProfit = parseInt(this.value);
        localStorage[KEY_MIN_PROFIT] = minProfit;
        switchBuying();
    };
    ui.appendChild(minProfitInput);
    ui.appendChild(createSpace());
    ui.appendChild(document.createTextNode("Number of items: "));
    var maxItemsInput = document.createElement("INPUT");
    maxItemsInput.value = maxItems;
    maxItemsInput.onchange = function () {
        maxItems = parseInt(this.value);
        localStorage[KEY_MAX_ITEMS] = maxItems;
        switchBuying();
    };
    ui.appendChild(maxItemsInput);
    ui.appendChild(createSpace());
    buyCountdown = document.createTextNode("Disabled");
    ui.appendChild(buyCountdown);
}

loadCache();

function loadCache() {
    forEachItem(function (i) {
        if (i === null) return false;
        if (localStorage[i.cacheKey] !== undefined) {
            var profit = getFromStorage(localStorage, i.cacheKey, 0) - i.getPrice();
            setProfit(i, profit);
        }
        return true;
    });
    sortPrices();
}

function setProfit(item, profit) {
    item.priceField.value = profit + CY;
    item.priceField.style.color = profit > 0 ? "green" : "red";
    item.isPriced = true;
    pricedCount++;
}

if (curItem !== null && curItem.onSelect()) {
    search();
}

function filterPrices(prices) {
    if (prices.length > 0) {
        for (var i = 0; i < prices.length; i++) {
            var price = prices[i];
            if (price.isNPC()) continue;
            localStorage[curItem.cacheKey] = price.valueOf();
            var profit = price.valueOf() - curItem.getPrice();
            setProfit(curItem, profit);
            break;
        }
    } else {
        curItem.priceField.value = "(!) " + curItem.resetPrice() + CY;
        curItem.priceField.style.color = "orange";
    }
    sortPrices();
}

function sortPrices() {
    sortedItems = [];
    forEachItem(function (i) {
        if (i === null) return false;
        sortedItems.push(i);
        return true;
    });
    sortedItems.sort(comparePrices);
    function comparePrices(a, b) {
        if (!b.isPriced) return -1e100;
        if (!a.isPriced) return 1e100;
        return b.getPrice() - a.getPrice();
    }
    for (var i = 0; i < sortedItems.length; i++) {
        sortedItems[i].itemDiv.parentNode.appendChild(sortedItems[i].itemDiv);
    }
    switchBuying();
}

function switchBuying() {
    if (buyCountdown === null) return;
    highlightItems();
    function highlightItems() {
        for (var i = 0; i < sortedItems.length; i++) {
            var item = sortedItems[i];
            item.priceField.style.fontWeight =
                (item.isPriced && item.getPrice() < minProfit) ? "normal" : "bold";
        }
    }
    var item = getMostProfitableItem();
    function getMostProfitableItem() {
        var spText = document.getElementsByClassName("widget-login-sp")[0].innerText,
            sp = parseInt(replaceAll(spText, ",", ""));
        for (var i = 0; i < sortedItems.length; i++) {
            if (sortedItems[i].actualPrice <= sp) {
                return sortedItems[i];
            }
        }
        return null;
    }
    if (!isBuying || !isSortingComplete || maxItems <= 0 || item === null ||
        item.getPrice() < minProfit) {
        clearInterval(buyingIntervalId);
        clearTimeout(buyingTimeoutId);
        if (isBuying && isSortingComplete && maxItems > 0 && item !== null) {
            buyCountdown.data = "Go to random shop...";
            goToNextShop();
        }
        else {
            buyCountdown.data =
                (isSortingComplete || !isBuying) ? "Disabled" : getProgress();
        }
        if (item === null) {
            buyCountdown.data = "Not enough sP!";
        }
        return;
    }
    function getProgress() {
        var k = 8,
            p = Math.floor(pricedCount/k),
            l = Math.floor(sortedItems.length/k) - p;
        return "Sorting " + new Array(p).join("█") + new Array(l).join("░");
    }
    var count = 5;
    buyCountdown.data = count.toString();
    buyingIntervalId = setInterval(buyItem, 1000);
    function buyItem() {
        count--;
        buyCountdown.data = count.toString();
        if (count > 0) return;
        clearInterval(buyingIntervalId);
        buyCountdown.data = "Buying...";
        sessionStorage[KEY_SHOP_URL] = location.href;
        buyingTimeoutId = setTimeout(click, 1000);
        function click() {
            maxItems--;
            localStorage[KEY_MAX_ITEMS] = maxItems;
            item.itemDiv.getElementsByClassName("ui image")[0].click();
        }
    }
}

function goToNextShop() {
    var shops = [36, 31, 28, 30, 32, 41, 11, 14, 34, 6, 29, 37, 16, 5, 19, 2, 22, 46, 49, 40,
            24, 45, 26, 23, 12, 44, 27, 39, 42, 9, 21, 4, 17, 20, 47, 25],
        url = location.href.split("="),
        iNext = Math.floor(Math.random()*shops.length),//shops.indexOf(parseInt(url[1])) + 1,
        next = url[0] + "=" + shops[(iNext >= shops.length) ? 0 : iNext].toString();
    open(next, "_self");
}

function goBackAfterBuying() {
    var url = sessionStorage[KEY_SHOP_URL];
    if (url === undefined) return;
    sessionStorage.removeItem(KEY_SHOP_URL);
    open(url, "_self");
}

onComplete();

function onComplete() {
    if (document.body.textContent.indexOf("There are no items stocked") > -1) {
        goToNextShop();
    }
}