🏠 Home 

Melvor Idle - AutoMastery

Automatically spends mastery when a pool is about to fill up

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