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.

// ==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;
}