IcePetsPlus

Some minor QOL improvements for icepets

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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()
})();