Melvor Idle - AutoMastery

Automatically spends mastery when a pool is about to fill up

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Melvor Idle - AutoMastery
// @description Automatically spends mastery when a pool is about to fill up
// @version     3.1
// @namespace   Visua
// @match       https://melvoridle.com/*
// @match       https://www.melvoridle.com/*
// @grant       none
// ==/UserScript==
/* jshint esversion: 6 */

((main) => {
    var script = document.createElement('script');
    script.textContent = `try { (${main})(); } catch (e) { console.log(e); }`;
    document.body.appendChild(script).parentNode.removeChild(script);
})(() => {
    'use strict';

    /**
     *
     * @param {number} skill
     * @param {number[]} masteries
     * @returns {{ id: number, xp: number, toNext: number }[]}
     */
    function getNonMaxedMasteries(skill, masteries) {
        return masteries.map(id => ({ id, xp: MASTERY[skill].xp[id], toNext: getMasteryXpForNextLevel(skill, id) })).filter(m => m.toNext > 0);
    }

    /**
     *
     * @param {{ id: number, xp: number, toNext: number }[]} masteries
     * @param {number} xpOverCheckpoint
     * @param {boolean} selectLowest
     * @returns
     */
    function getAffordableMastery(masteries, xpOverCheckpoint, selectLowest) {
        return masteries
            .reduce(
                (best, m) => {
                    if (m.toNext <= xpOverCheckpoint && (best.id === -1 || (selectLowest ? m.xp <= best.xp : m.xp >= best.xp))) {
                        return m;
                    } else {
                        return best;
                    }
                },
                { id: -1, xp: 0, toNext: 0 }
            ).id;
    }

    function autoSpendMasteryPool(skill, xpToBeAdded) {
        const poolXp = MASTERY[skill].pool;
        const poolMax = getMasteryPoolTotalXP(skill);
        if (poolXp + xpToBeAdded >= poolMax * AUTOMASTERY.settings[skill].spendWhenPoolReaches / 100) {
            const xpOverCheckpoint = (poolXp + xpToBeAdded) - (poolMax * AUTOMASTERY.settings[skill].threshold / 100);

            let masteryToLevel = -1;
            let reason = '';

            // Only look at selected non-maxed masteries
            let masteries = getNonMaxedMasteries(skill, AUTOMASTERY.settings[skill].selectedMasteries);
            if (!masteries.length) {
                // If no (non-maxed) masteries selected look at all masteries
                masteries = getNonMaxedMasteries(skill, MASTERY[skill].xp.map((_, id) => id));
            }

            if (!masteries.length) {
                return;
            }

            if (masteryToLevel === -1) {
                // Find the lowest or highest (depending on setting) mastery that can be afforded
                masteryToLevel = getAffordableMastery(masteries, xpOverCheckpoint, AUTOMASTERY.settings[skill].selectLowest);
                reason = `was the ${AUTOMASTERY.settings[skill].selectLowest ? 'lowest' : 'highest'} that could be leveled without dropping below ${AUTOMASTERY.settings[skill].threshold}%`;
            }

            if (masteryToLevel === -1) {
                // Find the cheapest mastery since we can't afford any
                const cheapest = masteries.reduce((cheapest, m) => m.toNext <= cheapest.toNext ? m : cheapest);
                if (cheapest.toNext < poolXp) {
                    masteryToLevel = cheapest.id;
                }
                reason = `was the cheapest to level and we are forced to drop below ${AUTOMASTERY.settings[skill].threshold}%`;
            }

            if (masteryToLevel !== -1) {
                const message = `AutoMastery: Leveled up ${getMasteryName(skill, masteryToLevel)} to ${getMasteryLevel(skill, masteryToLevel) + 1}`;
                const cost = getMasteryXpForNextLevel(skill, masteryToLevel);
                const details = `Earned ${numberWithCommas(xpToBeAdded.toFixed(3))} XP. `
                    + `Pool before: ${((poolXp / poolMax) * 100).toFixed(3)}%. `
                    + `Pool after: ${(((poolXp + xpToBeAdded - cost) / poolMax) * 100).toFixed(3)}%`;
                console.log(`${message} for ${numberWithCommas(Math.round(cost))} XP because it ${reason} (${details})`);
                autoMasteryNotify(message);
                const _showSpendMasteryXP = showSpendMasteryXP;
                showSpendMasteryXP = () => {};
                try {
                    levelUpMasteryWithPool(skill, masteryToLevel);
                } catch (e) {
                    console.error(e);
                } finally {
                    showSpendMasteryXP = _showSpendMasteryXP;
                }
                autoSpendMasteryPool(skill, xpToBeAdded);
            }
        }
    }

    function autoMasteryNotify(message) {
        Toastify({
            text: `<div class="text-center"><img class="notification-img" src="assets/media/main/mastery_pool.svg"><span class="badge badge-success">${message}</span></div>`,
            duration: 5000,
            gravity: 'bottom',
            position: 'center',
            backgroundColor: 'transparent',
            stopOnFocus: false,
        }).showToast();
    }

    function autoMastery() {
        // Load settings
        const settings = Object.keys(SKILLS).map(s => ({ threshold: 95, spendWhenPoolReaches: 100, selectLowest: true, selectedMasteries: [] }));
        const savedSettings = JSON.parse(localStorage.getItem(`AutoMastery-${currentCharacter}`));
        if (savedSettings) {
            settings.splice(0, savedSettings.length, ...savedSettings);
        }

        // Validate and save settings on change
        const settingsHandler = {
            set: function (obj, prop, value) {
                if (prop === 'threshold') {
                    if (!Number.isInteger(value)) {
                        throw new TypeError('threshold should be an integer');
                    }
                    if (value < 0 || value > 95) {
                        throw new RangeError('threshold should be a number from 0 to 95');
                    }
                } else if (prop === 'spendWhenPoolReaches') {
                    if (!Number.isInteger(value)) {
                        throw new TypeError('spendWhenPoolReaches should be an integer');
                    }
                    if (value < 0 || value > 100) {
                        throw new RangeError('spendWhenPoolReaches should be a number from 0 to 100');
                    }
                } else if (prop === 'selectLowest') {
                    if (typeof value !== 'boolean') {
                        throw new TypeError('selectLowest should be a boolean');
                    }
                } else if (prop === 'selectedMasteries') {
                    if (!Array.isArray(value) || value.some(e => !Number.isInteger(e))) {
                        throw new TypeError('selectedMasteries should be an array of integers');
                    }
                }

                obj[prop] = value;
                localStorage.setItem(`AutoMastery-${currentCharacter}`, JSON.stringify(AUTOMASTERY.settings));
                console.log('Settings saved');
                return true;
            },
        };

        window.AUTOMASTERY = {
            settings: settings.map(skillSettings => new Proxy(skillSettings, settingsHandler)),
        };

        // Inject
        const _addMasteryXPToPool = addMasteryXPToPool;
        addMasteryXPToPool = (...args) => {
            const _masteryPoolLevelUp = masteryPoolLevelUp;
            masteryPoolLevelUp = 1;
            try {
                const skill = args[0];
                let xpToBeAdded = args[1];
                const token = args[3];
                if (xpToBeAdded > 0) {
                    if (skillLevel[skill] >= 99 && !token) {
                        xpToBeAdded /= 2;
                    } else if (!token) {
                        xpToBeAdded /= 4;
                    }
                    autoSpendMasteryPool(skill, xpToBeAdded);
                }
            } catch (e) {
                console.error(e);
            } finally {
                masteryPoolLevelUp = _masteryPoolLevelUp;
                _addMasteryXPToPool(...args);
            }
        };
    }

    function loadScript() {
        if (typeof confirmedLoaded !== 'undefined' && confirmedLoaded) {
            clearInterval(interval);
            console.log('Loading AutoMastery');
            autoMastery();
        }
    }

    const interval = setInterval(loadScript, 500);
});