IcePetsPlus

Some minor QOL improvements for icepets

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         IcePetsPlus
// @namespace
// @version      1.0
// @description  Some minor QOL improvements for icepets
// @author       Cullen
// @match        https://www.icepets.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=icepets.com
// @grant        GM_addStyle
// @namespace https://greasyfork.org/users/428778
// ==/UserScript==
(function() {
    'use strict';

    const globals = {
        sortAsc: false
    }

    // RUN THIS THROUGH THIS SOMETIMES
    // https://beautifier.io/

    GM_addStyle(`
		input:not([type='text']), button {
			padding: 5px 10px;
			margin: 1px;
			cursor: pointer;
		}

		.copyTooltip {
		  position: fixed;
		  z-index: 1000;
		  padding: 5px 10px;
		  border: 1px solid #111;
		  background: #f2f2f5;
		  font-size: 12px;
		  font-family: verdana;
		}

		.tableheader td {
			cursor:pointer;
		}
	`);

    const routes = () => {
        // Globals
        setupToolstips()

        // Routed
        const path = window.location.pathname;
        const search = window.location.search;
        switch (path) {
            case '/halipar-jungle/companion-reserve':
            case '/snowslide-mountains/snow-jar-igloo':
            case '/glacia/page-turners':
            case '/glacia/frozen-collectives-emporium':
            case '/glacia/post-office':
            case '/glacia/plushie-palace':
            case '/glacia/toy-trunk':
            case '/misty-isle/grooming-parlour':
            case '/glacia/glacial-grocer':
            case '/misty-isle/battle-shop':
            case '/snowslide-mountains/sugar-rush':
            case '/snowslide-mountains/affogato':
            case '/misty-isle/golden-touch':
                setupShopPage();
                break;
            case '/halipar-jungle/collectors-quest':
            case '/quests/collect.php':
                setupCollectorPage();
                break;
            case '/usershops.php':
                search == '?stock' ? setupShopStockPage() : null;
                break;
            case '/arcade/gs.php':
                setupSlotsPage();
                break;
            default:
                break;
        }
    }

    // Notes/weirdthings in comments below

    /*
    const apiURL = "https://new.icepets.com/api/v1/search/items?item=&category=Companion&perPage=9999";

    fetch(apiURL)
      .then(response => response.json())
      .then(data => {
    	const filteredObjects = data.data.map(obj => !obj.slug.includes("-") ? obj.name : null).filter(x=>x!=null);
    	console.log(filteredObjects);
      })
      .catch(error => {
    	console.error("Error:", error);
      });

    */

    // https://www.icepets.com/snowslide-mountains/found-snow-jar

    const setupCollectorPage = () => {
        console.log('setupCollectorPage')
        document.querySelectorAll('table table tbody td img').forEach(questItem => {
            const substrStart = questItem.src.indexOf('/items/') + 7;
            const substrEnd = questItem.src.length - 4

            const itemId = questItem.src.substring(substrStart, substrEnd);

            const button = document.createElement('button')
            button.style.transition = "background-color 0.2s ease-out";
            button.style.width = '90px';
            button.style.margin = '1px'
            button.innerHTML = 'Buy';
            button.addEventListener('click', function() {
                button.innerHTML = 'Checking...';
                _buyScrapItem(itemId, button);
            });

            questItem.parentElement.append(button)
        })
    }

    const setupShopPage = () => {
        console.log('setupShopPage')
        const timerCountdown = () => {
            const timer = document.querySelector('b')
            const time = timer.innerText.split(' seconds')[0]
            let timeInSeconds = time.indexOf(':') != -1 ? Number(time.split(':')[0] * 60) + Number(time.split(':')[1]) : time;
            timeInSeconds--;
            const minutes = Math.floor(timeInSeconds / 60);
            const seconds = (timeInSeconds % 60).toLocaleString('en-US', {
                minimumIntegerDigits: 2,
                useGrouping: false
            })
            if (timeInSeconds == -1) return
            timer.innerHTML = minutes == 0 ? `${seconds} seconds` : `${minutes}:${seconds} seconds`
            // Wtf workers make timeouts work even in inactive tabs
            // https://stackoverflow.com/questions/5927284/how-can-i-make-setinterval-also-work-when-a-tab-is-inactive-in-chrome/5927432#12522580
            const blob = new Blob(["setTimeout(function(){postMessage('')}, 1000)"])
            const worker = new Worker(window.URL.createObjectURL(blob))
            worker.onmessage = timerCountdown;
        }

        timerCountdown()
    }

    const setupShopStockPage = () => {
        console.log('setupShopStockPage')
        // Copy "Update Stock" button to the top

        const updateButtonCopy = document.querySelector('.submitbutton').cloneNode(true);
        document.querySelector('.submitbutton').parentElement.prepend(updateButtonCopy)
        updateButtonCopy.after(document.createElement('br'))
        updateButtonCopy.after(document.createElement('br'))

        // Add sorting to the stock table
        // https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript/49041392#49041392
        const headers = document.querySelectorAll('tr.tableheader td');
        headers.forEach(th => th.addEventListener('click', (() => _sortTableByHeader(th, headers))));

        // Sort manually the first time to sort price low to high
        const th = document.querySelectorAll('tr.tableheader td')[2];
        _sortTableByHeader(th, headers);
    }

    const setupSlotsPage = () => {
        const verify = document.querySelector('input[name="gs_verify"]')

        if (verify) verify.focus();
        document.body.addEventListener('keyup', (e) => {
            if (e.key == 'Enter') document.querySelector('input[name="submit"]').click()
        })

    }

    const setupToolstips = () => {
        if (['/news.php', '/newboards/viewposts.php'] // Skip these
            .indexOf(window.location.pathname) != -1) return

        console.log('setupToolstips')
        GM_addStyle(`
			img[src*="images/items"] {
				cursor: pointer;
			}
		`);
        Array.from(document.querySelectorAll(':not(a) img[src*="images/items"]'))
            .filter(item => item.parentElement.tagName != 'A')
            .forEach(shopItem => {
                const itemName = _getItemNameFromNode(shopItem);
                shopItem.addEventListener('click', e => {
                    navigator.clipboard.writeText(itemName);
                    _addTooltip(shopItem, 'Copied item name!', e)
                });
            })
    }

    const _addTooltip = (element, text, e, speed) => {
        // Create a tooltip element.
        var tooltip = document.createElement('div');
        tooltip.textContent = text;
        tooltip.className = 'copyTooltip'
        tooltip.style.top = (e.clientY + 20) + "px";
        tooltip.style.left = (e.clientX + 20) + "px"

        // Add the tooltip to the DOM.
        element.parentNode.prepend(tooltip);
        setTimeout(() => _removeFadeOut(tooltip, speed ?? 500), 1000);
    }

    const _buyScrapItem = (itemId, button, price) => {
        const XHR = new XMLHttpRequest();
        const FD = new FormData();
        const dataFormat = {
            offer_price: price ?? '1618',
            action_buy: 'Buy Another'
        }

        for (const [name, value] of Object.entries(dataFormat)) {
            FD.append(name, value);
        }

        XHR.addEventListener("load", (event) => {
            //console.log("Yeah! Data sent and response loaded.", event);
        });

        XHR.addEventListener("error", (event) => {
            _setButtonSuccess(false, 'Oops! Error', button)
        });

        XHR.onreadystatechange = () => {
            if (XHR.readyState == 4) {
                if (XHR.status == 200) {
                    // The request was successful
                    if (XHR.responseText.indexOf('The item you were purchasing is no longer available')) {
                        _setButtonSuccess(true, 'Bought! :)', button)
                    } else {
                        _setButtonSuccess(false, 'Nope :(', button)
                    }
                } else {
                    _setButtonSuccess(false, 'Oops! Error', button)
                }
                return false
            }
        };

        XHR.open("POST", `https://www.icepets.com/scrapshop/index.php?act=buyitem&item=${itemId}`);

        XHR.send(FD);
    }

    const _checkAllWordsStartWithCapitalLetter = (str) => {
        const words = str?.split(' ');
        if (!words) return false
        for (const word of words) {
            if (word[0]?.toUpperCase() !== word[0]) {
                return false;
            }
        }
        return true;
    }

    const _checkTokenDabuRewards = (shopItem) => {
        if (!shopItem?.parentElement?.childNodes) return undefined
        return Array.from(shopItem?.parentElement?.childNodes)
            .filter(node => node?.tagName == "STRONG" &&
                _checkAllWordsStartWithCapitalLetter(node?.textContent) &&
                node?.children?.length == 0 &&
                node?.textContent?.indexOf(':') == -1
            )[0]?.textContent
    }

    const _checkNovitariaQuest = (shopItem) => {
        if (!shopItem?.parentElement?.childNodes) return undefined
        return Array.from(shopItem?.parentElement?.childNodes)
            .filter(node => node?.wholeText && node?.textContent.length > 2 && node?.textContent?.length < 100)[0]
            .textContent
    }

    const _compareSortingValues = (v1, v2) => {
        const validVersions = v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)
        return validVersions ? v1 - v2 : v1.toString().localeCompare(v2)
    }

    const _getCellValue = (tr, idx) => {
        return tr.children[idx].children[0]?.value || // input fields
            tr.children[idx]?.innerText || // text from first node
            tr.children[idx]?.textContent; // text from all child nodes
    }

    const _getItemNameFromNode = (shopItem) => {
        return shopItem?.parentElement?.querySelector('b')?.innerHTML ?? // NPC Shops
            _checkTokenDabuRewards(shopItem) ?? // Token Dabu reward
            shopItem?.parentElement?.querySelectorAll('strong')?.[2]?.innerHTML ?? // Beauty King
            shopItem?.parentElement?.querySelector('strong')?.innerHTML ?? // Collector Quest, Shop Purchase Confirmation, Cube Grab Main Page, Storage
            (shopItem?.alt.length > 0 ? shopItem?.alt : null) ?? // Solitary Sprite
            _checkNovitariaQuest(shopItem) ?? // Novitaria Quest
            shopItem?.parentElement?.lastChild?.textContent ?? // Scratchcard prizes, Shop Stock
            ''; // Uhhhhh
    }

    const _removeFadeOut = (el, speed) => {
        var seconds = speed / 1000;
        el.style.transition = "opacity " + seconds + "s ease";

        el.style.opacity = 0;
        setTimeout(function() {
            el.parentNode.removeChild(el);
        }, speed);
    }

    const _setButtonSuccess = (success, message, button) => {
        button.innerHTML = message;
        button.style.backgroundColor = success ? '#8fdf96' : '#df8f8f';

        setTimeout(() => {
            button.style.backgroundColor = '#d6e7ff';
            button.innerHTML = 'Buy';
        }, 1250)
    }

    const _sortTableByHeader = (th, headers) => {
        const table = th.closest('table');
        const tableRows = Array.from(th.parentNode.children);
        const thIndex = tableRows.indexOf(th)

        headers.forEach(th2 => {
            th2.innerHTML = th2.innerHTML.replace(' ↓', '').replace(' ↑', '')
        })
        th.innerHTML += globals.sortAsc ? ' ↑' : ' ↓';

        Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
            .sort(_sortTableSortHandler(thIndex))
            .forEach(tr => table.appendChild(tr));
    }

    const _sortTableSortHandler = (idx) => {
        globals.sortAsc = !globals.sortAsc;
		// .sort((a, b) => yourFunctionBlock)
        return (a, b) => {
            return _compareSortingValues(_getCellValue(globals.sortAsc ? a : b, idx), _getCellValue(globals.sortAsc ? b : a, idx));
        }
    }

    routes()
})();