Elethor General Purpose

Provides some general additions to Elethor

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Elethor General Purpose
// @description  Provides some general additions to Elethor
// @namespace    https://www.elethor.com/
// @version      1.7.17
// @author       Xortrox
// @contributor  Kidel
// @contributor  Saya
// @contributor  Archeron
// @contributor  Hito
// @match        https://elethor.com/*
// @match        https://www.elethor.com/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    const currentUserData = {};

    const moduleName = 'Elethor General Purpose';
    const version = '1.7.9';

    const profileURL = '/profile/';

    function initializeXHRHook() {
        let rawSend = XMLHttpRequest.prototype.send;

        XMLHttpRequest.prototype.send = function() {
            if (!this._hooked) {
                this._hooked = true;

                this.addEventListener('readystatechange', function() {
                    if (this.readyState === XMLHttpRequest.DONE) {
                        setupHook(this);
                    }
                }, false);
            }
            rawSend.apply(this, arguments);
        }

        function setupHook(xhr) {
            if (window.elethorGeneralPurposeOnXHR) {
                const e = new Event('EGPXHR');
                e.xhr = xhr;

                window.elethorGeneralPurposeOnXHR.dispatchEvent(e);
            }
        }
        window.elethorGeneralPurposeOnXHR = new EventTarget();

        console.log(`[${moduleName} v${version}] XHR Hook initialized.`);
    }

    function initializeUserLoadListener() {
        elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', function (e) {
            console.log('user load?:', e?.xhr?.responseURL);
            if (e && e.xhr
                && e.xhr.responseURL
                && e.xhr.responseURL.endsWith
                && e.xhr.responseURL.endsWith('/game/user')
            ) {
                try {
                    const userData = JSON.parse(e.xhr.responseText);

                    if (userData) {
                        for (const key of Object.keys(userData)) {
                            currentUserData[key] = userData[key];
                        }
                    }
                } catch (e) {
                    console.log(`[${moduleName} v${version}] Error parsing userData:`, e);
                }

            }
        });

        console.log(`[${moduleName} v${version}] User Load Listener initialized.`);
    }

    function initializeToastKiller() {
        document.addEventListener('click', function(e) {
            if (e.target
                && e.target.className
                && e.target.className.includes
                && e.target.className.includes('toasted toasted-primary')
            ) {
                e.target.remove();
            }
        });

        console.log(`[${moduleName} v${version}] Toast Killer initialized.`);
    }

    async function forceLoadUser() {
        const userLoad = await getUserSelf();
    }

    function initializeInventoryStatsLoadListener() {
        elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', function (e) {
            if (e && e.xhr
                && e.xhr.responseURL
                && e.xhr.responseURL.endsWith
                && e.xhr.responseURL.endsWith('/game/inventory/stats')
            ) {
                setTimeout(async () => {
                    if (Object.keys(currentUserData).length === 0) {
                        await forceLoadUser();
                    }

                    updateEquipmentPercentageSummary();

                    setTimeout(updateInventoryStatsPercentages, 1000);
                });
            }
        });

        console.log(`[${moduleName} v${version}] Inventory Stats Load Listener initialized.`);
    }

    function updateEquipmentPercentageSummary() {
        document.querySelector('.contains-equipment>div>div:nth-child(1)').setAttribute('style', 'width: 50%');
        document.querySelector('.contains-equipment>div>div:nth-child(2)').setAttribute('style', 'width: 25%');
        let percentagesTable = document.querySelector('#egpPercentagesSummary')
        if (!percentagesTable){
            percentagesTable = document.querySelector('.contains-equipment>div>div:nth-child(2)').cloneNode(true);
            percentagesTable.setAttribute('style', 'width: 25%');
            percentagesTable.id='egpPercentagesSummary';
            document.querySelector('.contains-equipment>div').appendChild(percentagesTable);

            for (const child of percentagesTable.children[0].children) {
                if (child && child.children && child.children[0]) {
                    child.children[0].remove();
                }
            }

            document.querySelector('#egpPercentagesSummary>table>tr:nth-child(8)').setAttribute('style', 'height:43px');
        }
    }

    function getStatSummary(equipment) {
        const summary = {
            base: {},
            energizements: {}
        };

        if (equipment) {
            for (const key of Object.keys(equipment)) {
                const item = equipment[key];

                /**
                 * Sums base attributes by name
                 * */
                if (item && item.attributes) {
                    for (const attributeName of Object.keys(item.attributes)) {
                        const attributeValue = item.attributes[attributeName];

                        if (!summary.base[attributeName]) {
                            summary.base[attributeName] = 0;
                        }

                        summary.base[attributeName] += attributeValue;
                    }
                }

                /**
                 * Sums energizements by stat name
                 * */
                if (item && item.upgrade && item.upgrade.energizements) {
                    for (const energizement of item.upgrade.energizements) {
                        if (!summary.energizements[energizement.stat]) {
                            summary.energizements[energizement.stat] = 0;
                        }

                        summary.energizements[energizement.stat] += Number(energizement.boost);
                    }
                }
            }
        }

        return summary;
    }

    function updateInventoryStatsPercentages() {
        let percentagesTable = document.querySelector('#egpPercentagesSummary')
        if (percentagesTable && currentUserData && currentUserData.equipment){
            const statSummary = getStatSummary(currentUserData.equipment);

            const baseKeys = Object.keys(statSummary.base);
            const energizementKeys = Object.keys(statSummary.energizements);

            let allKeys = baseKeys.concat(energizementKeys);
            const filterUniques = {};
            for (const key of allKeys){
                filterUniques[key] = true;
            }
            allKeys = Object.keys(filterUniques);
            allKeys.sort();

            allKeys.push('actions');

            const tableRows = percentagesTable.children[0].children;

            for(const row of tableRows) {
                if (row
                    && row.children
                    && row.children[0]
                    && row.children[0].children[0]
                ) {
                    const rowText = row.children[0].children[0];
                    rowText.innerText = '';
                }
            }

            let rowIndex = 0;
            for (const key of allKeys) {
                if (key === 'puncture') {
                    continue;
                }

                const row = tableRows[rowIndex];
                if (row
                    && row.children
                    && row.children[0]
                    && row.children[0].children[0]
                ) {
                    const rowText = row.children[0].children[0];

                    const rowBase = statSummary.base[key] || 0;
                    const rowEnergizement = (statSummary.energizements[key] || 0);
                    const rowEnergizementPercentage = (statSummary.energizements[key] || 0) * 100;

                    if (key.startsWith('+')) {
                        rowText.innerText = `${key} per 10 levels: ${rowEnergizement}`;
                    } else if (key === 'actions') {
                        const actions = currentUserData.user.bonus_actions || 0;
                        rowText.innerText = `Bonus Actions: ${actions}`;
                    } else {
                        rowText.innerText = `${key}: ${rowBase} (${rowEnergizementPercentage.toFixed(0)}%)`;
                    }

                    rowIndex++;
                }
            }
        }
    }

    function initializeLocationChangeListener() {
        let previousLocation = window.location.href;

        window.elethorGeneralPurposeOnLocationChange = new EventTarget();

        window.elethorLocationInterval = setInterval(() => {
            if (previousLocation !== window.location.href) {
                previousLocation = window.location.href;

                const e = new Event('EGPLocation');
                e.newLocation = window.location.href;
                window.elethorGeneralPurposeOnLocationChange.dispatchEvent(e);
            }

        }, 500);

        console.log(`[${moduleName} v${version}] Location Change Listener initialized.`);
    }

    function getProfileCombatElement() {
        const skillElements = document.querySelectorAll('.is-round-skill .progressbar-text>div>p:first-child');
        const skillElements2 = document.querySelectorAll('.is-round-skill .progressbar-text>div>p:nth-child(2)');

        let index = 0;
        for (const skillElement of skillElements2) {
            if (skillElement.innerText?.toLowerCase().includes('combat')) {
                return skillElements[index].parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
            }
            index++;
        }
    }

    function getProfileMiningElement() {
        const skillElements = document.querySelectorAll('.is-round-skill .progressbar-text>div>p:first-child');
        const skillElements2 = document.querySelectorAll('.is-round-skill .progressbar-text>div>p:nth-child(2)');

        let index = 0;
        for (const skillElement of skillElements2) {
            if (skillElement.innerText?.toLowerCase().includes('mining')) {
                return skillElements[index].parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
            }
            index++;
        }
    }

    function updateXPTracker(difference) {
        const combatElement = getProfileCombatElement();
        if (combatElement) {
            if (difference.combat > 0) {
                combatElement.setAttribute('data-combat-experience-ahead', `(+${formatNormalNumber(difference.combat)})`);
                combatElement.setAttribute('style', `color:lime`);
            } else {
                combatElement.setAttribute('data-combat-experience-ahead', `(${formatNormalNumber(difference.combat)})`);
                combatElement.setAttribute('style', `color:red`);
            }
        }

        const miningElement = getProfileMiningElement();
        if (difference.mining > 0) {
            miningElement.setAttribute('data-mining-experience-ahead', `(+${formatNormalNumber(difference.mining)})`);
            miningElement.setAttribute('style', `color:lime`);
        } else {
            miningElement.setAttribute('data-mining-experience-ahead', `(${formatNormalNumber(difference.mining)})`);
            miningElement.setAttribute('style', `color:red`);
        }
    }

    function initializeProfileLoadListener() {
        let css = '[data-combat-experience-ahead]::after { content: attr(data-combat-experience-ahead); padding: 12px;}';
        css += '[data-mining-experience-ahead]::after { content: attr(data-mining-experience-ahead); padding: 12px;}';

        appendCSS(css);

        window.elethorGeneralPurposeOnLocationChange.addEventListener('EGPLocation', async function (e) {
            if (e && e.newLocation) {
                if(e.newLocation.includes('/profile/')) {
                    console.log('Profile view detected:', e);
                    const url = e.newLocation;
                    const path = url.substr(url.indexOf(profileURL));

                    // We know we have a profile lookup, and not user-data load if the length differs.
                    if (path.length > profileURL.length) {
                        const userId = Number(path.substr(path.lastIndexOf('/') + 1));

                        const difference = await getExperienceDifference(userId, currentUserData.user.id);

                        updateXPTracker(difference);
                    }
                }
            }
        });

        console.log(`[${moduleName} v${version}] Profile Load Listener initialized.`);
    }

    async function getUser(id) {
        const result = await window.axios.get(`/game/user/${id}?egpIgnoreMe=true`);
        return result.data;
    }

    window.getUser = getUser;

    async function getUserSelf() {
        const result = await window.axios.get(`/game/user`);
        return result.data;
    }

    window.getUserSelf = getUserSelf;

    async function getUserStats() {
        const result = await window.axios.get(`/game/inventory/stats`);
        return result.data;
    }

    window.getUserStats = getUserStats;

    async function getUserStatsJSON(pretty) {
        const stats = await getUserStats();

        if (pretty) {
            return JSON.stringify(stats, null, 2);
        }

        return JSON.stringify(stats);
    }

    window.getUserStatsJSON = getUserStatsJSON;

    function getUserCombatStats(user) {
        for (const skill of user.skills) {
            if (skill.name === 'combat') {
                return skill.pivot;
            }
        }
    }

    function getMiningStats(user) {
        for (const skill of user.skills) {
            if (skill.id === 1) {
                return skill.pivot;
            }
        }

        return 0;
    }

    async function getExperienceDifference(userId1, userId2) {
        const [user1, user2] = await Promise.all([
            getUser(userId1),
            getUser(userId2)
        ]);

        const combatStats1 = getUserCombatStats(user1);
        const miningStats1 = getMiningStats(user1);

        const combatStats2 = getUserCombatStats(user2);
        const miningStats2 = getMiningStats(user2);

        return {
            combat: combatStats2.experience - combatStats1.experience,
            mining: miningStats2.experience - miningStats1.experience,
        };
    }

    (async function run() {
        await waitForField(window, 'axios');
        forceLoadUser();
        initializeToastKiller();
        initializeXHRHook();
        initializeUserLoadListener();
        initializeInventoryStatsLoadListener();
        initializeLocationChangeListener();
        initializeProfileLoadListener();
        loadMarketRecyclobotVisualizer();

        console.log(`[${moduleName} v${version}] Loaded.`);
    })();

    (async function loadRerollDisableButtonModule() {
        async function waitForEcho() {
            return new Promise((resolve, reject) => {
                const interval = setInterval(() => {
                    if (window.Echo) {
                        clearInterval(interval);
                        resolve();
                    }
                }, 100);
            });
        }

        await waitForEcho();
        await waitForUser();

        elethorGeneralPurposeOnXHR.addEventListener('EGPXHR', async function (e) {
            if (e && e.xhr && e.xhr.responseURL) {
                if(e.xhr.responseURL.includes('/game/energize')) {
                    const itemID = e.xhr.responseURL.substr(e.xhr.responseURL.lastIndexOf('/')+1);
                    window.lastEnergizeID = Number(itemID);
                }
            }
        });
    })();

    (async function loadResourceNodeUpdater() {
        await waitForField(currentUserData, 'user');
        const user = await getUser(currentUserData.user.id);

        function updateExperienceRates() {
            document.querySelectorAll('#nodeContainer>div:not(:first-child)').forEach(async (node) => {
                visualizeResourceNodeExperienceRates(node, user)
            });

            function visualizeResourceNodeExperienceRates(node, user) {
                const purity = getNodePurityPercentage(node, user);
                const density = getNodeDensityPercentage(node, user);
                const experience = getNodeExperience(node, density, user);
                const ore = 16;
                const experienceRate = experience?.toFixed(2);
                const oreRate = getOreRate(density, purity, ore);

                node.children[0].children[0].setAttribute('data-after', `${experienceRate} xp/h ${oreRate} ore/h`);
            }

            function getNodePurityPercentage(node, user) {
                const column = node.children[0].children[0].children[2];
                if (!column || !column.querySelectorAll('.font-bold')[0]) return;
                const label = column.querySelectorAll('.font-bold')[0].parentElement;
                let percentage = Number(label.innerText.replace('%','').split(':')[1]);

                let miningLevel = getMiningLevel(user);
                percentage = percentage + (miningLevel * 0.1);

                return percentage;
            }

            function getNodeDensityPercentage(node, user) {
                const column = node.children[0].children[0].children[1];
                if (!column || !column.querySelectorAll('.font-bold')[0]) return;
                const label = column.querySelectorAll('.font-bold')[0].parentElement;
                let percentage = Number(label.innerText.replace('%','').split(':')[1]);

                let miningLevel = getMiningLevel(user);
                percentage = percentage + (miningLevel * 0.1);

                return percentage;
            }

            function getNodeExperience(node, density, user) {
                /** Gets the Exp: label */
                const column = node.children[0].children[0].children[3];
                if (!column || !column.querySelectorAll('.font-bold')[0]) return;
                const label = column.querySelectorAll('.font-bold')[0].parentElement;
                let value = Number(label.innerText.replace('%','').split(':')[1]);

                const skilledExtraction = getSkilledExtractionLevel(user);
                const knowledgeExtraction = getKnowledgeExtractionLevel(user) / 100;

                const ore = getActiveMining();
                const expertiseLevel = getExpertiseLevel(user);
                const expertiseGain = getExpertiseXPGainByOre(ore);
                const additionalMiningGain = expertiseLevel / 5 * expertiseGain;
                console.log('additionalMiningGain:', additionalMiningGain);

                value += additionalMiningGain;

                const actionsPerHour = getActionsPerHour(density);
                const experienceBase = value * actionsPerHour;
                const experienceSkilled = actionsPerHour * skilledExtraction;
                const experienceKnowledge = value * knowledgeExtraction;

                value = experienceBase + experienceSkilled + experienceKnowledge;

                const vip = isUserVIP(user);

                value *= vip ? 1.1 : 1;

                return value;
            }

            function getActionsPerHour(density) {
                return 3600 / (60 / (density / 100))
            }

            function getExperienceRate(density, experience) {
                return Number((3600 / (60 / (density / 100)) * experience).toFixed(2));
            }

            function getOreRate(density, purity, ore) {
                return Number((3600 / (60 / (density / 100)) * (purity / 100) * ore).toFixed(2));
            }
        }

        function isUserVIP(user) {
            return new Date(user.vip_expires) > new Date();
        }

        updateExperienceRates();
        window.elethorResourceInterval = setInterval(updateExperienceRates, 500);
        initializeResourceNodeView();

        async function initializeResourceNodeView() {
            await waitForField(document, 'head');

            let css = '[data-after]::after { content: attr(data-after); padding: 12px;}';

            appendCSS(css);
        }
    })();

    async function loadMarketRecyclobotVisualizer() {
        if (!window.egpItemPoints) {
            const companions = await getCompanions();
            const recyclobot = companions.recyclobot;
            window.egpItemPoints = recyclobot.itemPoints;

            console.log(`[${moduleName} v${version}] Companion Data initialized.`);
        }

        window.elethorGeneralPurposeOnLocationChange.addEventListener('EGPLocation', async function (e) {
            if (e && e.newLocation) {
                window.egpRecyclobotOnMarket = e.newLocation.includes('/market/');

                if (!window.egpRecyclobotOnMarket) {
                    return;
                }

                if (!window.egpItemPoints) {
                    console.warn(`[${moduleName} v${version}] Entered market page but had no recyclobot item points list.`);
                    return;
                }

                const marketObjects = getMarketListingObjects();

                for (const marketListing of marketObjects) {
                    for (const recyclobotEntry of window.egpItemPoints) {
                        if (recyclobotEntry.item_name === marketListing.name) {
                            //marketListing.element.setAttribute('data-points-recyclobot',`R: ${recyclobotEntry.quantity}:${recyclobotEntry.points}`);
                            break;
                        }
                    }
                }
            }
        });

        function getMarketListingObjects() {
            return Array.prototype.slice.call(document.querySelectorAll('.is-market-side-menu ul li')).map((e) => {return {element: e, name: e.innerText}});
        }

        async function getCompanions() {
            return (await window.axios.get('/game/companions')).data;
        }
    };

    function setPackAmount(amount) {
        window.packAmount = amount;

        if (window.packAmount < 0) {
            window.packAmount = 0;
        }
    }

    window.setPackAmount = setPackAmount;

    function formatNormalNumber(num){
        return num.toLocaleString();
    }

    function appendCSS(css) {
        let head = document.head || document.getElementsByTagName('head')[0];
        let style = document.createElement('style');

        head.appendChild(style);

        style.type = 'text/css';
        if (style.styleSheet){
            // This is required for IE8 and below.
            style.styleSheet.cssText = css;
        } else {
            style.appendChild(document.createTextNode(css));
        }
    }

    function getMiningLevel(user) {
        for (const skill of user.skills) {
            if (skill.id === 1) {
                return skill.pivot.level;
            }
        }

        return 0;
    }

    function getSkilledExtractionLevel(user) {
        for (const skill of user.skills) {
            if (skill.id === 17) {
                return skill.pivot.level;
            }
        }

        return 0;
    }

    function getKnowledgeExtractionLevel(user) {
        for (const skill of user.skills) {
            if (skill.id === 18) {
                return skill.pivot.level;
            }
        }

        return 0;
    }

    function getExpertiseLevel(user) {
        for (const skill of user.skills) {
            if (skill.id === 38) {
                return skill.pivot.level;
            }
        }

        return 0;
    }

    expertiseXPGainByOre = {
        'Orthoclase': 780,
        'Anorthite': 825,
        'Ferrisum': 600,
        'Rhenium': 465,
        'Jaspil': 480,
        'Skasix': 0,
    };

    function getExpertiseXPGainByOre(ore) {
        return expertiseXPGainByOre[ore];
    }

    function getActiveMining() {
        const activeMiningButtons = document.querySelectorAll('.buttons .button.is-success');

        if (!activeMiningButtons[0] ||
            !activeMiningButtons[0].children[0] ||
            !activeMiningButtons[0].children[0].innerText ||
            !activeMiningButtons[0].children[0].innerText.trim
        ) {
            return '';
        }

        return activeMiningButtons[0].children[0].innerText.trim();
    }

    function getPackLevel(user) {
        for (const skill of user.skills) {
            if (skill.id === 22) {
                return skill.pivot.level;
            }
        }

        return 0;
    }

    function getCarnageLevel(user) {
        for (const skill of user.skills) {
            if (skill.id === 23) {
                return skill.pivot.level;
            }
        }

        return 0;
    }

    function getAttackSpeed(speed) {
        return 50 - (50 * speed / (speed + 400));
    }

    function getHealth(fortitude) {
        return 100000 * fortitude / (fortitude + 4000);
    }

    async function waitForField(target, field) {
        return new Promise((resolve, reject) => {
            const interval = setInterval(() => {
                if (target[field] !== undefined) {
                    clearInterval(interval);
                    resolve();
                }
            }, 100);
        });
    }

    async function waitForUser() {
        return new Promise((resolve, reject) => {
            const interval = setInterval(() => {
                if (currentUserData.user && currentUserData.user.id !== undefined) {
                    clearInterval(interval);
                    resolve();
                }
            }, 100);
        });
    }

    (async function loadAlarms() {
        await waitForField(window, 'Echo');
        await waitForField(document, 'head');
        await waitForUser();


        const meta1 = document.createElement('meta');
        meta1.setAttribute('content', "media-src https://furious.no/elethor/sound/");
        meta1.setAttribute('http-equiv', 'Content-Security-Policy');
        document.getElementsByTagName('head')[0].appendChild(meta1);

        /**
         * You can add an alternative audio by setting the local storage keys:
         * localStorage.setItem('egpSoundOutOfActions', 'your-url')
         * localStorage.setItem('egpSoundQuestCompleted', 'your-url')
         * */

        // Alternative: https://furious.no/elethor/sound/elethor-actions-have-ran-out.mp3
        window.egpSoundOutOfActions =
            localStorage.getItem('egpSoundOutOfActions') || 'https://furious.no/elethor/sound/out-of-actions.wav';

        // Alternative: https://furious.no/elethor/sound/elethor-quest-completed.mp3
        window.egpSoundQuestCompleted =
            localStorage.getItem('egpSoundQuestCompleted') || 'https://furious.no/elethor/sound/quest-complete.wav';

        const masterAudioLevel = 0.3;

        window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\Monster\\Events\\FightingAgain", handleActionData);
        window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\ResourceNode\\Events\\GatheringAgain", handleActionData);
        window.Echo.private(`App.User.${currentUserData.user.id}`).listen(".App\\Domain\\Quest\\Events\\UpdateQuestProgress", handleQuestData);

        let hasWarnedAboutActions = false;
        let hasWarnedAboutQuest = false;

        /**
         * Warns once when action runs out.
         * */
        function handleActionData(data) {
            if (data && data.action) {
                if (data.action.remaining <= 0 && !hasWarnedAboutActions) {
                    playOutOfActions();
                    hasWarnedAboutActions = true;
                } else if (data.action.remaining > 0 && hasWarnedAboutActions) {
                    hasWarnedAboutActions = false;
                }
            }
        }
        window.handleActionData = handleActionData;

        /**
         * Warns once when quest completes.
         * */
        function handleQuestData(data) {
            if (data
                && data.step
                && data.step.tasks) {
                if (data.step.progress >= data.step.tasks[0].quantity && !hasWarnedAboutQuest) {
                    playQuestCompleted();
                    hasWarnedAboutQuest = true;
                } else if (data.step.progress < data.step.tasks[0].quantity && hasWarnedAboutQuest) {
                    hasWarnedAboutQuest = false;
                }
            }
        }
        window.handleQuestData = handleQuestData;

        function playOutOfActions() {
            playSound(window.egpSoundOutOfActions);
        }
        window.playOutOfActions = playOutOfActions;

        function playQuestCompleted() {
            playSound(window.egpSoundQuestCompleted);
        }
        window.playQuestCompleted = playQuestCompleted;

        function playSound(sound, volume = masterAudioLevel){
            const audio = new Audio(sound);

            audio.volume = volume;
            audio.play();
        }
    })();
})();