Melvor Idle - AutoMastery

Automatically spends mastery when a pool is about to fill up

  1. // ==UserScript==
  2. // @name Melvor Idle - AutoMastery
  3. // @description Automatically spends mastery when a pool is about to fill up
  4. // @version 3.1
  5. // @namespace Visua
  6. // @match https://melvoridle.com/*
  7. // @match https://www.melvoridle.com/*
  8. // @grant none
  9. // ==/UserScript==
  10. /* jshint esversion: 6 */
  11.  
  12. ((main) => {
  13. var script = document.createElement('script');
  14. script.textContent = `try { (${main})(); } catch (e) { console.log(e); }`;
  15. document.body.appendChild(script).parentNode.removeChild(script);
  16. })(() => {
  17. 'use strict';
  18.  
  19. /**
  20. *
  21. * @param {number} skill
  22. * @param {number[]} masteries
  23. * @returns {{ id: number, xp: number, toNext: number }[]}
  24. */
  25. function getNonMaxedMasteries(skill, masteries) {
  26. return masteries.map(id => ({ id, xp: MASTERY[skill].xp[id], toNext: getMasteryXpForNextLevel(skill, id) })).filter(m => m.toNext > 0);
  27. }
  28.  
  29. /**
  30. *
  31. * @param {{ id: number, xp: number, toNext: number }[]} masteries
  32. * @param {number} xpOverCheckpoint
  33. * @param {boolean} selectLowest
  34. * @returns
  35. */
  36. function getAffordableMastery(masteries, xpOverCheckpoint, selectLowest) {
  37. return masteries
  38. .reduce(
  39. (best, m) => {
  40. if (m.toNext <= xpOverCheckpoint && (best.id === -1 || (selectLowest ? m.xp <= best.xp : m.xp >= best.xp))) {
  41. return m;
  42. } else {
  43. return best;
  44. }
  45. },
  46. { id: -1, xp: 0, toNext: 0 }
  47. ).id;
  48. }
  49.  
  50. function autoSpendMasteryPool(skill, xpToBeAdded) {
  51. const poolXp = MASTERY[skill].pool;
  52. const poolMax = getMasteryPoolTotalXP(skill);
  53. if (poolXp + xpToBeAdded >= poolMax * AUTOMASTERY.settings[skill].spendWhenPoolReaches / 100) {
  54. const xpOverCheckpoint = (poolXp + xpToBeAdded) - (poolMax * AUTOMASTERY.settings[skill].threshold / 100);
  55.  
  56. let masteryToLevel = -1;
  57. let reason = '';
  58.  
  59. // Only look at selected non-maxed masteries
  60. let masteries = getNonMaxedMasteries(skill, AUTOMASTERY.settings[skill].selectedMasteries);
  61. if (!masteries.length) {
  62. // If no (non-maxed) masteries selected look at all masteries
  63. masteries = getNonMaxedMasteries(skill, MASTERY[skill].xp.map((_, id) => id));
  64. }
  65.  
  66. if (!masteries.length) {
  67. return;
  68. }
  69.  
  70. if (masteryToLevel === -1) {
  71. // Find the lowest or highest (depending on setting) mastery that can be afforded
  72. masteryToLevel = getAffordableMastery(masteries, xpOverCheckpoint, AUTOMASTERY.settings[skill].selectLowest);
  73. reason = `was the ${AUTOMASTERY.settings[skill].selectLowest ? 'lowest' : 'highest'} that could be leveled without dropping below ${AUTOMASTERY.settings[skill].threshold}%`;
  74. }
  75.  
  76. if (masteryToLevel === -1) {
  77. // Find the cheapest mastery since we can't afford any
  78. const cheapest = masteries.reduce((cheapest, m) => m.toNext <= cheapest.toNext ? m : cheapest);
  79. if (cheapest.toNext < poolXp) {
  80. masteryToLevel = cheapest.id;
  81. }
  82. reason = `was the cheapest to level and we are forced to drop below ${AUTOMASTERY.settings[skill].threshold}%`;
  83. }
  84.  
  85. if (masteryToLevel !== -1) {
  86. const message = `AutoMastery: Leveled up ${getMasteryName(skill, masteryToLevel)} to ${getMasteryLevel(skill, masteryToLevel) + 1}`;
  87. const cost = getMasteryXpForNextLevel(skill, masteryToLevel);
  88. const details = `Earned ${numberWithCommas(xpToBeAdded.toFixed(3))} XP. `
  89. + `Pool before: ${((poolXp / poolMax) * 100).toFixed(3)}%. `
  90. + `Pool after: ${(((poolXp + xpToBeAdded - cost) / poolMax) * 100).toFixed(3)}%`;
  91. console.log(`${message} for ${numberWithCommas(Math.round(cost))} XP because it ${reason} (${details})`);
  92. autoMasteryNotify(message);
  93. const _showSpendMasteryXP = showSpendMasteryXP;
  94. showSpendMasteryXP = () => {};
  95. try {
  96. levelUpMasteryWithPool(skill, masteryToLevel);
  97. } catch (e) {
  98. console.error(e);
  99. } finally {
  100. showSpendMasteryXP = _showSpendMasteryXP;
  101. }
  102. autoSpendMasteryPool(skill, xpToBeAdded);
  103. }
  104. }
  105. }
  106.  
  107. function autoMasteryNotify(message) {
  108. Toastify({
  109. text: `<div class="text-center"><img class="notification-img" src="assets/media/main/mastery_pool.svg"><span class="badge badge-success">${message}</span></div>`,
  110. duration: 5000,
  111. gravity: 'bottom',
  112. position: 'center',
  113. backgroundColor: 'transparent',
  114. stopOnFocus: false,
  115. }).showToast();
  116. }
  117.  
  118. function autoMastery() {
  119. // Load settings
  120. const settings = Object.keys(SKILLS).map(s => ({ threshold: 95, spendWhenPoolReaches: 100, selectLowest: true, selectedMasteries: [] }));
  121. const savedSettings = JSON.parse(localStorage.getItem(`AutoMastery-${currentCharacter}`));
  122. if (savedSettings) {
  123. settings.splice(0, savedSettings.length, ...savedSettings);
  124. }
  125.  
  126. // Validate and save settings on change
  127. const settingsHandler = {
  128. set: function (obj, prop, value) {
  129. if (prop === 'threshold') {
  130. if (!Number.isInteger(value)) {
  131. throw new TypeError('threshold should be an integer');
  132. }
  133. if (value < 0 || value > 95) {
  134. throw new RangeError('threshold should be a number from 0 to 95');
  135. }
  136. } else if (prop === 'spendWhenPoolReaches') {
  137. if (!Number.isInteger(value)) {
  138. throw new TypeError('spendWhenPoolReaches should be an integer');
  139. }
  140. if (value < 0 || value > 100) {
  141. throw new RangeError('spendWhenPoolReaches should be a number from 0 to 100');
  142. }
  143. } else if (prop === 'selectLowest') {
  144. if (typeof value !== 'boolean') {
  145. throw new TypeError('selectLowest should be a boolean');
  146. }
  147. } else if (prop === 'selectedMasteries') {
  148. if (!Array.isArray(value) || value.some(e => !Number.isInteger(e))) {
  149. throw new TypeError('selectedMasteries should be an array of integers');
  150. }
  151. }
  152.  
  153. obj[prop] = value;
  154. localStorage.setItem(`AutoMastery-${currentCharacter}`, JSON.stringify(AUTOMASTERY.settings));
  155. console.log('Settings saved');
  156. return true;
  157. },
  158. };
  159.  
  160. window.AUTOMASTERY = {
  161. settings: settings.map(skillSettings => new Proxy(skillSettings, settingsHandler)),
  162. };
  163.  
  164. // Inject
  165. const _addMasteryXPToPool = addMasteryXPToPool;
  166. addMasteryXPToPool = (...args) => {
  167. const _masteryPoolLevelUp = masteryPoolLevelUp;
  168. masteryPoolLevelUp = 1;
  169. try {
  170. const skill = args[0];
  171. let xpToBeAdded = args[1];
  172. const token = args[3];
  173. if (xpToBeAdded > 0) {
  174. if (skillLevel[skill] >= 99 && !token) {
  175. xpToBeAdded /= 2;
  176. } else if (!token) {
  177. xpToBeAdded /= 4;
  178. }
  179. autoSpendMasteryPool(skill, xpToBeAdded);
  180. }
  181. } catch (e) {
  182. console.error(e);
  183. } finally {
  184. masteryPoolLevelUp = _masteryPoolLevelUp;
  185. _addMasteryXPToPool(...args);
  186. }
  187. };
  188. }
  189.  
  190. function loadScript() {
  191. if (typeof confirmedLoaded !== 'undefined' && confirmedLoaded) {
  192. clearInterval(interval);
  193. console.log('Loading AutoMastery');
  194. autoMastery();
  195. }
  196. }
  197.  
  198. const interval = setInterval(loadScript, 500);
  199. });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址