Grundos Cafe Quest Cost Calculator

Calculates the cost of the Grundos Cafe Quest items as the user searches for them on the Shop Wiz. Displays the info in a table above the Quest Items when the Wiz Searches finished loading in the other tabs.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Grundos Cafe Quest Cost Calculator
// @namespace    https://www.grundos.cafe
// @version      2.0
// @description  Calculates the cost of the Grundos Cafe Quest items as the user searches for them on the Shop Wiz. Displays the info in a table above the Quest Items when the Wiz Searches finished loading in the other tabs.
// @author       Dark_Kyuubi
// @match        https://www.grundos.cafe/market/wizard*
// @match        https://www.grundos.cafe/winter/snowfaerie*
// @match        https://www.grundos.cafe/halloween/witchtower*
// @match        https://www.grundos.cafe/halloween/esophagor*
// @match        https://www.grundos.cafe/island/kitchen*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=grundos.cafe
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.deleteValue
// @grant        GM.addValueChangeListener
// @license      MIT
// ==/UserScript==

class QuestItems {
    constructor(questGiver, items, deadline) {
        this.questGiver = questGiver;
        this.items = items;
        this.deadline = deadline;
    }
}

class Item {
    constructor(name, cost) {
        this.name = name;
        this.cost = cost;
    }
}

//Map serialization and proper Map checking is pain
let quests = [];
let questsGrid = document.createElement('div');

const styles = `
    .quest-item-qcc,.quest-giver,.final-cost {
        width: 100%;
        height: 100%;
        border: 1px solid black;
`
let styleSheet = document.createElement("style");

(async function () {
    'use strict';

    await initializeQuests();

    if (!window.location.href.includes("wizard")) {
        let questGiver = determineQuestGiver();
        if (window.location.href.includes("complete")) {
            if (!document.querySelector("strong.red")) {
                quests = quests.filter(q => q.questGiver !== questGiver);
                if (quests.length === 0) {
                    await GM.deleteValue('quests');
                } else {
                    await updateStoredQuests();
                }
            }
        } else {
            pushStyle();
            GM.addValueChangeListener('quests', async (name, oldValue, newValue, remote) => {
                if (remote && oldValue !== newValue) {
                    quests = newValue ? JSON.parse(newValue) : [];
                    showCalculatedQuestsSum(questGiver);
                }
            });
            updateGrid(questGiver);
        }
    } else {
        let shopWizResult = document.querySelector('.sw_results');
        let swItem = document.querySelector('p.mt-1>strong')?.innerHTML.substring(18, undefined).trim();
        if (quests.length > 0 && shopWizResult && isQuestItem(swItem)) {
            await collectShopWizPrice(swItem);
        }
    }

    async function updateGrid(questGiver) {
        await getNewQuestItems(questGiver);
        showCalculatedQuestsSum(questGiver);
    }

    function isQuestItem(swItem) {
        return quests.some(q => q.items.some(i => i.name === swItem));
    }

    function showCalculatedQuestsSum(questGiver) {
        questsGrid.setAttribute('style', getGridStyle());
        questsGrid.className = 'quest-grid';
        questsGrid.innerHTML = '';
        createQuestGiverHeaders();
        createQuestItemRows();
        createQuestSumRow();
        const insertLocation = getInsertLocationByQuestGiver(questGiver);
        if (insertLocation) {
            if (!questsGrid.parentNode) {
                insertLocation.before(questsGrid);
            }
        }
    }

    function getInsertLocationByQuestGiver(questGiver) {
        switch (questGiver) {
            case 'snowfaerie':
                return document.getElementById('taelia_grid') === null ? document.querySelector('.itemList') : document.getElementById('taelia_grid');
            case 'edna':
                return document.querySelector('.itemList');
            case 'esophagor':
                return document.querySelector('.itemList');
            case 'kitchen':
                return document.querySelector('.itemList');
        }
    }

    function getGridStyle() {
        let gridStyle = 'display: grid; border: 1px solid black;text-align: center;';
        gridStyle += 'grid-template-columns: repeat(' + quests.length + ', 1fr);';
        return gridStyle;
    }

    function createQuestItemRows() {
        for (let i = 0; i < 4; i++) {
            for (const quest of quests) {
                let itemName = quest.items[i]?.name;
                if (!itemName) {
                    itemName = "-";
                }
                questsGrid.innerHTML += '<div class="quest-item-qcc">' + itemName + '</div>';
            };
        }
    }

    function createQuestGiverHeaders() {
        for (const quest of quests) {
            questsGrid.innerHTML += '<div class="quest-giver">' + quest.questGiver + '</div>';
        };
    }

    function createQuestSumRow() {
        for (const quest of quests) {
            let items = quest.items;
            let sum = 0;
            for (const item of items) {
                sum += item.cost || 0;
            }
            questsGrid.innerHTML += '<div class="final-cost">' + sum + '</div>';
        }
    }

    async function collectShopWizPrice(swItem) {
        let swPrice = parseInt(document.querySelector('.sw_results>.data>strong')?.innerHTML.match(/\d+/g).join(''));
        if (swItem && swPrice) {
            await updatePriceForQuestItems(swItem, swPrice);
            await updateStoredQuests();
        }
    }

    async function getNewQuestItems(questGiver) {
        let questItemsElements = document.querySelectorAll('.centered-item').length > 0 ? document.querySelectorAll('.centered-item') : document.querySelectorAll('.quest-item');
        if (questItemsElements.length > 0) {
            let questItems = getQuestItems(questItemsElements);
            let deadline = getDeadline();
            let existingQuestIndex = quests.findIndex(q => q.questGiver === questGiver);
            if (existingQuestIndex !== -1) {
                let needsToBeUpdated = itemsOutdated(questItems, existingQuestIndex);
                if (needsToBeUpdated) {
                    quests[existingQuestIndex] = new QuestItems(questGiver, questItems, deadline);
                    await updateStoredQuests();
                }
            } else {
                quests.push(new QuestItems(questGiver, questItems, deadline));
                await updateStoredQuests();
            }
        }
    }

    function itemsOutdated(questItems, existingQuestIndex) {
        let itemNames = quests[existingQuestIndex].items.map(i => i.name);
        return questItems.some(i => !itemNames.includes(i.name));
    }

    function getDeadline() {
        var xpath = "//span[contains(text(),'minutes')]";
        const questCountDownAfterAccepting = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
        var matchingElement = questCountDownAfterAccepting ? questCountDownAfterAccepting : document.querySelector('.taelia_countdown');
        if (matchingElement) {
            const matchingElementText = matchingElement.innerText;
            let hours = parseInt(matchingElementText.match(/(\d+)\s*hrs/)?.[1] || 0);
            let minutes = parseInt(matchingElementText.match(/(\d+)\s*minutes/)?.[1] || 0);
            let seconds = parseInt(matchingElementText.match(/(\d+)\s*seconds/)?.[1] || 0);
            let deadline = new Date(Date.now() + (hours * 60 * 60 * 1000) + (minutes * 60 * 1000) + (seconds * 1000)).getTime();
            return deadline;
        }
        return null;
    }

    function pushStyle() {
        styleSheet.innerText = styles;
        document.head.appendChild(styleSheet);
    }
})();

async function initializeQuests() {
    const storedQuestsRaw = await GM.getValue('quests');
    const storedQuests = storedQuestsRaw ? JSON.parse(storedQuestsRaw) : [];
    if (storedQuests && !(storedQuests instanceof Array)) {
        const parsedStoredQuests = JSON.parse(storedQuests)?.reduce((m, [key, val]) => m.set(key, val), new Map());
        if (parsedStoredQuests instanceof Map) {
            await GM.deleteValue('quests');
        }
    } else if (storedQuests) {
        quests = storedQuests.filter(questIem => questIem.deadline > Date.now());
    }
}

async function updateStoredQuests() {
    await GM.setValue('quests', JSON.stringify(quests));
}

async function updatePriceForQuestItems(swItem, swPrice) {
    const storedQuestsRaw = await GM.getValue('quests');
    const latestQuests = storedQuestsRaw ? JSON.parse(storedQuestsRaw) : [];
    latestQuests.forEach(quest => {
        quest.items.forEach(item => {
            if (item.name === swItem) {
                item.cost = swPrice;
            }
        })
    });
    quests = latestQuests;
}

function getQuestItems(questItemsElements) {
    let questItems = [];
    for (let i = 0; i < questItemsElements?.length; i++) {
        let questItemName = questItemsElements[i].querySelector('strong').innerHTML;
        let item = new Item(questItemName, null);
        questItems[i] = item;
    }
    return questItems;
}

function determineQuestGiver() {
    if (window.location.href.includes('snowfaerie')) {
        return 'snowfaerie';
    } else if (window.location.href.includes('witchtower')) {
        return 'edna';
    } else if (window.location.href.includes('esophagor')) {
        return 'esophagor';
    } else if (window.location.href.includes('kitchen')) {
        return 'kitchen';
    }
    return null;
}