// ==UserScript==
// @name Melvor TimeRemaining
// @namespace http://tampermonkey.net/
// @version 0.6.2.3
// @description Shows time remaining for completing a task with your current resources. Takes into account Mastery Levels and other bonuses.
// @author Breindahl#2660
// @match https://melvoridle.com/*
// @match https://www.melvoridle.com/*
// @match https://melvoridle.com/*
// @match https://test.melvoridle.com/*
// @grant none
// ==/UserScript==
/* jshint esversion: 9 */
// Note that this script is made for Melvor Idle version 0.17
// Later versions might break parts of this script
// Big thanks to Xhaf#6478, Visua#9999 and TinyCoyote#1769 for helping with parts of the code and troubleshooting
// settings can be toggled from the console, or edited here
window.timeRemainingSettings = {
// true for 12h clock (AM/PM), false for 24h clock
IS_12H_CLOCK: false,
// true for short clock `xxhxxmxxs`, false for long clock `xx hours, xx minutes and xx seconds`
IS_SHORT_CLOCK: false,
// true for alternative main display with xp/h, mastery xp/h and action count
SHOW_XP_RATE: false,
// true to allow final pool percentage > 100%
UNCAP_POOL: false,
};
(function () {
function injectScript(main) {
var script = document.createElement('script');
script.textContent = `try {(${main})();} catch (e) {console.log(e);}`;
document.body.appendChild(script).parentNode.removeChild(script);
}
function script() {
// Loading script
console.log('Melvor TimeRemaining Loaded');
// Function to check if task is complete
function taskComplete(skillID) {
if (window.timeLeftLast > 1 && window.timeLeftCurrent === 0) {
notifyPlayer(skillID,"Task Done","danger");
console.log('Melvor TimeRemaining: task done');
let ding = new Audio("https://www.myinstants.com/media/sounds/ding-sound-effect.mp3");
ding.volume=0.1;
ding.play();
}
}
// Create timeLeft containers
let TempContainer = ['<div class="font-size-sm font-w600 text-uppercase text-center text-muted"><small id ="','" class="mb-2" style="display:block;clear:both;white-space: pre-line" data-toggle="tooltip" data-placement="top" data-html="true" title="" data-original-title=""></small></div>'];
let TempContainerAlt = ['<div class="font-size-sm text-uppercase text-muted"><small id ="','" class="mt-2" style="display:block;clear:both;white-space: pre-line" data-toggle="tooltip" data-placement="top" data-html="true" title="" data-original-title=""></small></div>'];
$("#smith-item-have").after(TempContainer[0] + "timeLeftSmithing" + TempContainer[1]);
$("#fletch-item-have").after(TempContainer[0] + "timeLeftFletching" + TempContainer[1]);
$("#runecraft-item-have").after(TempContainer[0] + "timeLeftRunecrafting" + TempContainer[1]);
$("#craft-item-have").after(TempContainer[0] + "timeLeftCrafting" + TempContainer[1]);
$("#herblore-item-have").after(TempContainer[0] + "timeLeftHerblore" + TempContainer[1]);
$("#skill-cooking-food-selected-qty").after(TempContainerAlt[0] + "timeLeftCooking" + TempContainerAlt[1]);
$("#skill-fm-logs-selected-qty").after(TempContainerAlt[0] + "timeLeftFiremaking" + TempContainerAlt[1]);
$("#magic-item-have-and-div").after(TempContainer[0] + "timeLeftMagic" + TempContainer[1]);
// Mastery Pool progress
for(let id in SKILLS) {
if(SKILLS[id].hasMastery) {
let bar = $(`#mastery-pool-progress-${id}`)[0];
$(bar).after(`<div id="mastery-pool-progress-end-${id}" class="progress-bar bg-warning" role="progressbar" style="width: 0%; background-color: #e5ae679c !important;"></div>`);
}
}
// Mastery Progress bars
for(let id in SKILLS) {
if(SKILLS[id].hasMastery) {
let name = skillName[id].toLowerCase();
let bar = $(`#${name}-mastery-progress`)[0];
$(bar).after(`<div id="${id}-mastery-pool-progress-end" class="progress-bar bg-info" role="progressbar" style="width: 0%; background-color: #5cace59c !important;"></div>`);
}
}
// Mastery Skill progress
for(let id in SKILLS) {
if(SKILLS[id].hasMastery) {
let bar = $(`#skill-progress-bar-${id}`)[0];
$(bar).after(`<div id="skill-progress-bar-end-${id}" class="progress-bar bg-info" role="progressbar" style="width: 0%; background-color: #5cace59c !important;"></div>`);
}
}
// Function to get unformatted number for Qty
function getQtyOfItem(itemID) {
for (let i = 0; i < bank.length; i++) {
if (bank[i].id === itemID) {
return bank[i].qty;
}
}
return 0;
}
let appendName = (t, name, isShortClock) => {
if (t === 0) {
return "";
}
if (isShortClock) {
return t + name[0];
}
let result = t + " " + name;
if (t === 1) {
return result;
}
return result + "s";
};
// Convert seconds to hours/minutes/seconds and format them
function secondsToHms(d, isShortClock = timeRemainingSettings.IS_SHORT_CLOCK) {
d = Number(d);
// split seconds in hours, minutes and seconds
let h = Math.floor(d / 3600);
let m = Math.floor(d % 3600 / 60);
let s = Math.floor(d % 3600 % 60);
// no comma in short form
// ` and ` if hours and minutes or hours and seconds
// `, ` if hours and minutes and seconds
let hDisplayComma = " ";
if (!isShortClock && h > 0) {
if ((m === 0 && s > 0) || (s === 0 && m > 0)) {
hDisplayComma = " and ";
} else if (s > 0 && m > 0) {
hDisplayComma = ", ";
}
}
// no comma in short form
// ` and ` if minutes and seconds
let mDisplayComma = " ";
if (!isShortClock && m > 0 && s > 0) {
mDisplayComma = " and ";
}
// append h/hour/hours etc depending on isShortClock, then concat and return
return appendName(h, "hour", isShortClock) + hDisplayComma +
appendName(m, "minute", isShortClock) + mDisplayComma +
appendName(s, "second", isShortClock);
}
// Add seconds to date
function AddSecondsToDate(date, seconds) {
return new Date(date.getTime() + seconds*1000);
}
function daysBetween(now, then) {
const startOfDayNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
return Math.floor((then - startOfDayNow) / 1000 / 60 / 60 / 24 + (startOfDayNow.getTimezoneOffset() - then.getTimezoneOffset()) / (60 * 24));
}
// Format date 24 hour clock
function DateFormat(now, then, is12h = timeRemainingSettings.IS_12H_CLOCK, isShortClock = timeRemainingSettings.IS_SHORT_CLOCK){
let days = daysBetween(now, then);
days = (days == 0) ? "" : (days == 1) ? " tomorrow" : ` + ${days}` + (isShortClock ? "d" : " days");
let hours = then.getHours();
let minutes = then.getMinutes();
// convert to 12h clock if required
let amOrPm = '';
if (is12h) {
amOrPm = hours >= 12 ? 'pm' : 'am';
hours = (hours % 12) || 12;
}
// pad numbers
hours = hours < 10 ? '0' + hours : hours;
minutes = minutes < 10 ? '0' + minutes : minutes;
// concat and return remaining time
return hours + ':' + minutes + amOrPm + days;
}
// Level to XP Array
const lvlToXp = Array.from({ length: 200 }, (_, i) => exp.level_to_xp(i));
// Convert level to XP needed to reach that level
function convertLvlToXP(level) {
if (level === Infinity) { return Infinity; }
let xp = 0;
if (level === 1) { return xp; }
xp = lvlToXp[level] + 1;
return xp;
}
// Convert XP value to level
function convertXPToLvl(xp, noCap = false) {
let level = 1;
while (lvlToXp[level] < xp) { level++; }
level--;
if (level < 1) { level = 1; }
else if (!noCap && level > 99) { level = 99; }
return level;
}
// Get Mastery Level of given Skill and Mastery ID
function getMasteryLevel(skill, masteryID) {
return convertXPToLvl(MASTERY[skill].xp[masteryID]);
}
// Progress in current level
function getPercentageInLevel(currentXP, finalXP, type, bar = false) {
let currentLevel = convertXPToLvl(currentXP, true);
if (currentLevel >= 99 && (type == "mastery" || bar == true)) return 0;
let currentLevelXP = convertLvlToXP(currentLevel);
let nextLevelXP = convertLvlToXP(currentLevel+1);
let diffLevelXP = nextLevelXP - currentLevelXP;
let currentLevelPercentage = (currentXP - currentLevelXP) / diffLevelXP * 100;
if (bar == true) {
let finalLevelPercentage = ((finalXP - currentXP) > (nextLevelXP - currentXP)) ? 100 - currentLevelPercentage : ((finalXP - currentXP)/diffLevelXP*100).toFixed(4);
return finalLevelPercentage;
}
else {
return currentLevelPercentage;
}
}
// Main function
function timeRemaining(skillID) {
// Reset variables
var masteryID = 0;
var skillInterval = 0;
var rhaelyxCharge = 0;
var chargeUses = 0;
var itemXP = 0;
var item = 0;
var initialTotalMasteryPoolXP = 0;
var masteryPoolMaxXP = 0;
var initialTotalMasteryLevelForSkill = 0;
var initialTotalMasteryXP = 0; // Current amount of Mastery experience
var masteryLim = []; // Xp needed to reach next level
var skillLim = []; // Xp needed to reach next level
var poolLim = []; // Xp need to reach next pool checkpoint
var skillReq = []; // Needed items for craft and their quantities
var itemCraft = []; // Amount of items craftable for each resource requirement
var recordCraft = Infinity; // Amount of craftable items for limiting resource
// Generate default values for script
var timeLeftID = "timeLeft".concat(skillName[skillID]); // Field for generating timeLeft HTML
var masteryLimLevel = Array.from({ length: 98 }, (_, i) => i + 2); //Breakpoints for mastery bonuses - default all levels starting at 2 to 99, followed by Infinity
masteryLimLevel.push(Infinity);
var skillLimLevel = Array.from({ length: 98 }, (_, i) => i + 2); //Breakpoints for mastery bonuses - default all levels starting at 2 to 99, followed by Infinity
skillLimLevel.push(Infinity);
var poolLimCheckpoints = [10,25,50,95,100,Infinity]; //Breakpoints for mastery pool bonuses followed by Infinity
var chanceToKeep = Array.from({ length: 99 }, (_, i) => i *0.002); // Chance to keep at breakpoints - default 0.2% per level
chanceToKeep[98] += 0.05; // Level 99 Bonus
var now = new Date(); // Current time and day
var initialSkillXP = skillXP[skillID]; // Current skill XP
// Set current skill and pull matching variables from game with script
switch (skillID) {
case CONSTANTS.skill.Smithing:
item = smithingItems[selectedSmith].itemID;
itemXP = items[item].smithingXP;
skillInterval = 2000;
if (godUpgrade[3]) skillInterval *= 0.8;
for (let i of items[item].smithReq) {
skillReq.push(i);
}
masteryLimLevel = [20,40,60,80,99,Infinity]; // Smithing Mastery Limits
chanceToKeep = [0,0.05,0.10,0.15,0.20,0.30]; //Smithing Mastery bonus percentages
if(petUnlocked[5]) chanceToKeep = chanceToKeep.map(n => n + PETS[5].chance/100); // Add Pet Bonus
break;
case CONSTANTS.skill.Fletching:
item = fletchingItems[selectedFletch].itemID;
itemXP = items[item].fletchingXP;
skillInterval = 2000;
if (godUpgrade[0]) skillInterval *= 0.8;
if (petUnlocked[8]) skillInterval -= 200;
for (let i of items[item].fletchReq) {
skillReq.push(i);
}
//Special Case for Arrow Shafts
if (item == 276) {
if (window.selectedFletchLog === undefined) {window.selectedFletchLog = 0;}
skillReq = [skillReq[window.selectedFletchLog]];
}
break;
case CONSTANTS.skill.Runecrafting:
item = runecraftingItems[selectedRunecraft].itemID;
itemXP = items[item].runecraftingXP;
skillInterval = 2000;
if (godUpgrade[1]) skillInterval *= 0.8;
for (let i of items[item].runecraftReq) {
skillReq.push(i);
}
masteryLimLevel = [99,Infinity]; // Runecrafting has no Mastery bonus
chanceToKeep = [0,0]; //Thus no chance to keep
if (equippedItems.includes(CONSTANTS.item.Runecrafting_Skillcape) || equippedItems.includes(CONSTANTS.item.Max_Skillcape) || equippedItems.includes(CONSTANTS.item.Cape_of_Completion)) chanceToKeep[0] += 0.35;
if (petUnlocked[10]) chanceToKeep[0] += PETS[10].chance/100;
chanceToKeep[1] = chanceToKeep[0];
break;
case CONSTANTS.skill.Crafting:
item = craftingItems[selectedCraft].itemID;
itemXP = items[item].craftingXP;
skillInterval = 3000;
if (godUpgrade[0]) skillInterval *= 0.8;
if (equippedItems.includes(CONSTANTS.item.Crafting_Skillcape) || equippedItems.includes(CONSTANTS.item.Max_Skillcape) || equippedItems.includes(CONSTANTS.item.Cape_of_Completion)) skillInterval -= 500;
if (petUnlocked[9]) skillInterval -= 200;
for (let i of items[item].craftReq) {
skillReq.push(i);
}
break;
case CONSTANTS.skill.Herblore:
item = herbloreItemData[selectedHerblore].itemID[getHerbloreTier(selectedHerblore)];
itemXP = herbloreItemData[selectedHerblore].herbloreXP;
skillInterval = 2000;
if (godUpgrade[1]) skillInterval *= 0.8;
for (let i of items[item].herbloreReq) {
skillReq.push(i);
}
break;
case CONSTANTS.skill.Cooking:
item = selectedFood;
itemXP = items[item].cookingXP;
if (currentCookingFire > 0) {
itemXP *= (1 + cookingFireData[currentCookingFire - 1].bonusXP / 100);
}
skillInterval = 3000;
if (godUpgrade[3]) skillInterval *= 0.8;
skillReq = [{id: item, qty: 1}];
masteryLimLevel = [99,Infinity]; //Cooking has no Mastery bonus
chanceToKeep = [0,0]; //Thus no chance to keep
item = items[item].cookedItemID;
break;
case CONSTANTS.skill.Firemaking:
item = selectedLog;
itemXP = logsData[selectedLog].xp * (1 + bonfireBonus / 100);
skillInterval = logsData[selectedLog].interval;
if (godUpgrade[3]) skillInterval *= 0.8;
skillReq = [{id: item, qty: 1}];
chanceToKeep.fill(0); // Firemaking Mastery does not provide preservation chance
break;
case CONSTANTS.skill.Magic:
skillInterval = 2000;
//Find need runes for spell
if (ALTMAGIC[selectedAltMagic].runesRequiredAlt !== undefined && useCombinationRunes) {
for (let i of ALTMAGIC[selectedAltMagic].runesRequiredAlt) {
skillReq.push({...i});
}
}
else {
for (let i of ALTMAGIC[selectedAltMagic].runesRequired) {
skillReq.push({...i});
}
}
// Get Rune discount
for (let i = 0; i < skillReq.length; i++) {
if (items[equippedItems[CONSTANTS.equipmentSlot.Weapon]].providesRune !== undefined) {
if (items[equippedItems[CONSTANTS.equipmentSlot.Weapon]].providesRune.includes(skillReq[i].id)) {
let capeMultiplier = 1;
if (equippedItems.includes(CONSTANTS.item.Magic_Skillcape) || equippedItems.includes(CONSTANTS.item.Max_Skillcape) || equippedItems.includes(CONSTANTS.item.Cape_of_Completion)) capeMultiplier = 2; // Add cape multiplier
skillReq[i].qty -= items[equippedItems[CONSTANTS.equipmentSlot.Weapon]].providesRuneQty * capeMultiplier;
}
}
}
skillReq = skillReq.filter(item => item.qty > 0); // Remove all runes with 0 cost
//Other items
if (ALTMAGIC[selectedAltMagic].selectItem == 1 && selectedMagicItem[1] !== null) { // Spells that just use 1 item
skillReq.push({id: selectedMagicItem[1], qty: 1});
}
else if (ALTMAGIC[selectedAltMagic].selectItem == -1) { // Spells that doesn't require you to select an item
if (ALTMAGIC[selectedAltMagic].needCoal) { // Rags to Riches II
skillReq.push({id: 48, qty: 1});
}
}
else if (selectedMagicItem[0] !== null && ALTMAGIC[selectedAltMagic].selectItem == 0) { // SUPERHEAT
for (let i of items[selectedMagicItem[0]].smithReq) {
skillReq.push({...i});
}
if (ALTMAGIC[selectedAltMagic].ignoreCoal) {
skillReq = skillReq.filter(item => item.id !== 48);
}
}
masteryLimLevel = [Infinity]; //AltMagic has no Mastery bonus
chanceToKeep = [0]; //Thus no chance to keep
break;
}
// Configure initial mastery values for all skills with masteries
if (skillID != CONSTANTS.skill.Magic) {
initialTotalMasteryPoolXP = MASTERY[skillID].pool;
masteryPoolMaxXP = getMasteryPoolTotalXP(skillID);
initialTotalMasteryLevelForSkill = getCurrentTotalMasteryLevelForSkill(skillID);
masteryID = items[item].masteryID[1];
initialTotalMasteryXP = MASTERY[skillID].xp[masteryID];
}
// Apply itemXP Bonuses from gear and pets
itemXP = addXPBonuses(skillID, itemXP, true);
// Populate masteryLim from masteryLimLevel
for (let i = 0; i < masteryLimLevel.length; i++) {
masteryLim[i] = convertLvlToXP(masteryLimLevel[i]);
}
// Populate skillLim from skillLimLevel
for (let i = 0; i < skillLimLevel.length; i++) {
skillLim[i] = convertLvlToXP(skillLimLevel[i]);
}
// Populate poolLim from masteryCheckpoints
for (let i = 0; i < poolLimCheckpoints.length; i++) {
poolLim[i] = masteryPoolMaxXP * poolLimCheckpoints[i] / 100;
}
// Check for Crown of Rhaelyx
var RhaelyxChance = 0.15;
if (equippedItems.includes(CONSTANTS.item.Crown_of_Rhaelyx) && skillID != CONSTANTS.skill.Magic) {
for (let i = 0; i < masteryLimLevel.length; i++) {
chanceToKeep[i] += 0.10; // Add base 10% chance
}
rhaelyxCharge = getQtyOfItem(CONSTANTS.item.Charge_Stone_of_Rhaelyx);
chargeUses = rhaelyxCharge * 1000; // Estimated uses from Rhaelyx Charge Stones
}
// Get Item Requirements and Current Requirements
for (let i = 0; i < skillReq.length; i++) {
let itemReq = skillReq[i].qty;
//Check how many of required resource in Bank
let itemQty = getQtyOfItem(skillReq[i].id);
// Calculate max items you can craft for each itemReq
itemCraft[i] = Math.floor(itemQty/itemReq);
// Calculate limiting factor and set new record
if(itemCraft[i] < recordCraft) {
recordCraft = itemCraft[i];
}
}
//Return the chanceToKeep for any mastery EXP
function masteryChance(masteryEXP, chanceToRefTable){
let chanceTo = chanceToRefTable;
if (masteryEXP >= masteryLim[0]) {
for (let i = 0; i < masteryLim.length; i++) {
if (masteryLim[i] <= masteryEXP && masteryEXP < masteryLim[i+1]) {
return chanceTo[i+1];
}
}
} else {return chanceTo[0];}
}
// Adjust interval based on unlocked bonuses
function intervalAdjustment(currentPoolMasteryXP, currentMasteryXP) {
let adjustedInterval = skillInterval;
switch (skillID) {
case CONSTANTS.skill.Fletching:
if (currentPoolMasteryXP >= poolLim[3]) adjustedInterval -= 200;
break;
case CONSTANTS.skill.Firemaking: {
if (currentPoolMasteryXP >= poolLim[1]) adjustedInterval *= 0.9;
let decreasedBurnInterval = 1 - convertXPToLvl(currentMasteryXP) * 0.001;
adjustedInterval *= decreasedBurnInterval;
break;
}
}
return adjustedInterval;
}
// Adjust preservation chance based on unlocked bonuses
function preservationAdjustment(currentPoolMasteryXP) {
let adjustedPreservation = 0;
switch (skillID) {
case CONSTANTS.skill.Smithing:
if (currentPoolMasteryXP >= poolLim[1]) adjustedPreservation += 5;
if (currentPoolMasteryXP >= poolLim[2]) adjustedPreservation += 5;
break;
case CONSTANTS.skill.Runecrafting:
if (currentPoolMasteryXP >= poolLim[2]) adjustedPreservation += 10;
break;
case CONSTANTS.skill.Herblore:
if (currentPoolMasteryXP >= poolLim[2]) adjustedPreservation += 5;
break;
case CONSTANTS.skill.Cooking:
if (currentPoolMasteryXP >= poolLim[2]) adjustedPreservation += 10;
break;
}
return adjustedPreservation / 100;
}
// Adjust skill XP based on unlocked bonuses
function skillXPAdjustment(currentPoolMasteryXP, currentMasteryXP) {
let xpMultiplier = 1;
switch (skillID) {
case CONSTANTS.skill.Runecrafting:
if (currentPoolMasteryXP >= poolLim[1] && items[item].type === "Rune") xpMultiplier += 1.5;
break;
case CONSTANTS.skill.Cooking: {
let burnChance = calcBurnChance(currentMasteryXP);
let cookXP = itemXP * (1 - burnChance);
let burnXP = 1 * burnChance;
return cookXP + burnXP;
}
}
return itemXP * xpMultiplier;
}
// Calculate total number of unlocked items for skill based on current skill level
function calcTotalUnlockedItems(currentTotalSkillXP) {
let count = 0;
let currentSkillLevel = convertXPToLvl(currentTotalSkillXP);
for (let i = 0; i < MILESTONES[skillName[skillID]].length; i++) {
if (currentSkillLevel >= MILESTONES[skillName[skillID]][i].level) count++;
}
return count;
}
// Calculate mastery xp based on unlocked bonuses
function calcMasteryXpToAdd(timePerAction, currentTotalSkillXP, currentMasteryXP, currentPoolMasteryXP, currentTotalMasteryLevelForSkill) {
let xpModifier = 1;
// General Mastery XP formula
let xpToAdd = (((calcTotalUnlockedItems(currentTotalSkillXP) * currentTotalMasteryLevelForSkill) / getTotalMasteryLevelForSkill(skillID) + convertXPToLvl(currentMasteryXP) * (getTotalItemsInSkill(skillID) / 10)) * (timePerAction / 1000)) / 2;
// Skill specific mastery pool modifier
if (currentPoolMasteryXP >= poolLim[0]) {
xpModifier += 0.05;
}
// Firemaking pool and log modifiers
if (skillID === CONSTANTS.skill.Firemaking) {
// If current skill is Firemaking, we need to apply mastery progression from actions and use updated currentPoolMasteryXP values
if (currentPoolMasteryXP >= poolLim[3]) {
xpModifier += 0.05;
}
for (let i = 0; i < MASTERY[CONSTANTS.skill.Firemaking].xp.length; i++) {
// The logs you are not burning
if (masteryID != i) {
if (getMasteryLevel(CONSTANTS.skill.Firemaking, i) >= 99) {
xpModifier += 0.0025;
}
}
}
// The log you are burning
if (convertXPToLvl(currentMasteryXP) >= 99) {
xpModifier += 0.0025;
}
} else {
// For all other skills, you use the game function to grab your FM mastery progression
if (getMasteryPoolProgress(CONSTANTS.skill.Firemaking) >= masteryCheckpoints[3]) {
xpModifier += 0.05;
}
for (let i = 0; i < MASTERY[CONSTANTS.skill.Firemaking].xp.length; i++) {
if (getMasteryLevel(CONSTANTS.skill.Firemaking, i) >= 99) {
xpModifier += 0.0025;
}
}
}
// Ty modifier
if (petUnlocked[21]) {
xpModifier += 0.03;
}
// AroM modifier
if (equippedItems.includes(CONSTANTS.item.Ancient_Ring_Of_Mastery)) {
xpModifier += items[CONSTANTS.item.Ancient_Ring_Of_Mastery].bonusMasteryXP;
}
// Combine base and modifiers
xpToAdd *= xpModifier;
if (xpToAdd < 1) {
xpToAdd = 1;
}
// BurnChance affects average mastery XP
if (skillID === CONSTANTS.skill.Cooking) {
let burnChance = calcBurnChance(currentMasteryXP);
xpToAdd *= (1 - burnChance);
}
return xpToAdd;
}
// Calculate pool XP based on mastery XP
function calcPoolXPToAdd(currentTotalSkillXP, masteryXP) {
if (convertXPToLvl(currentTotalSkillXP) >= 99) {return masteryXP / 2; }
else { return masteryXP / 4; }
}
// Calculate burn chance based on mastery level
function calcBurnChance(currentMasteryXP) {
let burnChance = 0;
if (equippedItems.includes(CONSTANTS.item.Cooking_Skillcape) || equippedItems.includes(CONSTANTS.item.Max_Skillcape) || equippedItems.includes(CONSTANTS.item.Cape_of_Completion)) {
return burnChance;
}
if (equippedItems.includes(CONSTANTS.item.Cooking_Gloves)) {
return burnChance;
}
let primaryBurnChance = (30 - convertXPToLvl(currentMasteryXP) * 0.6) / 100;
let secondaryBurnChance = 0.01;
if (primaryBurnChance <= 0) {
return secondaryBurnChance;
}
burnChance = 1 - (1 - primaryBurnChance) * (1 - secondaryBurnChance);
return burnChance;
}
// Calculates expected time, taking into account Mastery Level advancements during the craft
function calcExpectedTime(resources){
let sumTotalTime = 0;
let maxPoolTime = 0;
let maxMasteryTime = 0;
let maxSkillTime = 0;
let maxPoolReached = false;
let maxMasteryReached = false;
let maxSkillReached = false;
let maxXP = convertLvlToXP(99);
if (initialTotalMasteryPoolXP >= masteryPoolMaxXP) maxPoolReached = true;
if (initialTotalMasteryXP >= maxXP) maxMasteryReached = true;
if (initialSkillXP >= maxXP) maxSkillReached = true;
let currentTotalMasteryXP = initialTotalMasteryXP;
let currentTotalSkillXP = initialSkillXP;
let currentTotalPoolXP = initialTotalMasteryPoolXP;
let currentTotalMasteryLevelForSkill = initialTotalMasteryLevelForSkill;
// compute current xp/h and mxp/h
let initialInterval = intervalAdjustment(initialTotalMasteryPoolXP, initialTotalMasteryXP);
let xph = skillXPAdjustment(initialTotalMasteryPoolXP, initialTotalMasteryXP) / initialInterval * 1000 * 3600;
// compute current mastery xp / h using the getMasteryXpToAdd from the game
let masteryXPh = getMasteryXpToAdd(skillID, masteryID, initialInterval) / initialInterval * 1000 * 3600;
// alternative: compute through the calcMasteryXpToAdd method from this script, they should be the same !
// let masteryXPh = calcMasteryXpToAdd(initialInterval, currentTotalSkillXP, currentTotalMasteryXP, currentTotalPoolXP, currentTotalMasteryLevelForSkill) / initialInterval * 1000 * 3600;
// counter for estimated number of actions
let actions = 0;
while (resources > 0) {
// Adjustments
let currentPreservationAdjustment = preservationAdjustment(currentTotalPoolXP);
let totalChanceToUse = 1 - masteryChance(currentTotalMasteryXP,chanceToKeep) - currentPreservationAdjustment;
let currentInterval = intervalAdjustment(currentTotalPoolXP, currentTotalMasteryXP);
// Current Limits
let currentMasteryLim = masteryLim.find(element => element > currentTotalMasteryXP);
let currentSkillLim = skillLim.find(element => element > currentTotalSkillXP);
let currentPoolLim = poolLim.find(element => element > currentTotalPoolXP);
// Current XP
let currentMasteryXP = calcMasteryXpToAdd(currentInterval, currentTotalSkillXP, currentTotalMasteryXP, currentTotalPoolXP, currentTotalMasteryLevelForSkill);
let currentSkillXP = skillXPAdjustment(currentTotalPoolXP, currentTotalMasteryXP);
let currentPoolXP = calcPoolXPToAdd(currentTotalSkillXP, currentMasteryXP);
// Distance to Limits
let masteryXPToLimit = currentMasteryLim - currentTotalMasteryXP;
let skillXPToLimit = currentSkillLim - currentTotalSkillXP;
let poolXPToLimit = currentPoolLim - currentTotalPoolXP;
// Actions to limits
let masteryXPActions = masteryXPToLimit / currentMasteryXP;
let skillXPActions = skillXPToLimit / currentSkillXP;
let poolXPActions = poolXPToLimit / currentPoolXP;
// estimate amount of actions
// number of actions with rhaelyx charges
let resourceActions = Math.min(chargeUses, resources / (totalChanceToUse - RhaelyxChance));
// remaining resources
let resWithoutCharge = Math.max(0, resources - chargeUses);
// add number of actions without rhaelyx charges
resourceActions += x / totalChanceToUse;
// Minimum actions based on limits
let expectedActions = Math.ceil(Math.min(masteryXPActions, skillXPActions, poolXPActions, resourceActions));
// Take away resources based on expectedActions
if (expectedActions == resourceActions) {
resources = 0; // No more limits
} else {
let resUsed = 0;
if (expectedActions < chargeUses) {
// won't run out of charges yet
resUsed = expectedActions * (totalChanceToUse - RhaelyxChance);
} else {
// first use charges
resUsed = chargeUses * (totalChanceToUse - RhaelyxChance);
// remaining actions are without charges
resUsed += (expectedActions - chargeUses) * totalChanceToUse;
}
resources = Math.round(resources - resUsed);
}
// time for current loop
let timeToAdd = expectedActions * currentInterval;
// Update time and XP
sumTotalTime += timeToAdd;
currentTotalMasteryXP += currentMasteryXP*expectedActions;
currentTotalSkillXP += currentSkillXP*expectedActions;
currentTotalPoolXP += currentPoolXP*expectedActions;
// Time for max pool, 99 Mastery and 99 Skill
if (!maxPoolReached && currentTotalPoolXP >= masteryPoolMaxXP) {
maxPoolTime = sumTotalTime;
maxPoolReached = true;
}
if (!maxMasteryReached && maxXP <= currentTotalMasteryXP) {
maxMasteryTime = sumTotalTime;
maxMasteryReached = true;
}
if (!maxSkillReached && maxXP <= currentTotalSkillXP) {
maxSkillTime = sumTotalTime;
maxSkillReached = true;
}
// Update remaining Rhaelyx Charge uses
chargeUses -= expectedActions;
if ( chargeUses < 0 ) chargeUses = 0;
// Level up mastery if hitting Mastery limit
if ( masteryXPActions == expectedActions ) currentTotalMasteryLevelForSkill++;
// estimate total remaining actions
actions += expectedActions;
}
return {
"timeLeft" : Math.round(sumTotalTime),
"actions": actions,
"finalSkillXP" : currentTotalSkillXP,
"finalMasteryXP" : currentTotalMasteryXP,
"finalPoolPercentage" : Math.min((currentTotalPoolXP/masteryPoolMaxXP) * 100, timeRemainingSettings.UNCAP_POOL ? Infinity : 100).toFixed(2),
"maxPoolTime" : maxPoolTime,
"maxMasteryTime" : maxMasteryTime,
"maxSkillTime" : maxSkillTime,
"masteryXPh": masteryXPh,
"xph" : xph,
};
}
//Time left
var results = 0;
var timeLeft = 0;
var timeLeftPool = 0;
var timeLeftMastery = 0;
var timeLeftSkill = 0;
if (skillID == CONSTANTS.skill.Magic) {
timeLeft = Math.round(recordCraft * skillInterval / 1000);
} else {
results = calcExpectedTime(recordCraft);
timeLeft = Math.round(results.timeLeft / 1000);
timeLeftPool = Math.round(results.maxPoolTime / 1000);
timeLeftMastery = Math.round(results.maxMasteryTime / 1000);
timeLeftSkill = Math.round(results.maxSkillTime / 1000);
}
//Global variables to keep track of when a craft is complete
window.timeLeftLast = window.timeLeftCurrent;
window.timeLeftCurrent = timeLeft;
//Inject timeLeft HTML
let timeLeftElement = document.getElementById(timeLeftID);
if(timeLeftElement !== null) {
if (timeLeft !== 0) {
let finishedTime = AddSecondsToDate(now, timeLeft);
if (timeRemainingSettings.SHOW_XP_RATE) {
timeLeftElement.textContent = "XP/h: " + formatNumber(Math.floor(results.xph)) +
"\r\nMXP/h: " + formatNumber(Math.floor(results.masteryXPh)) +
"\r\nActions: " + formatNumber(results.actions) +
"\r\nTime: " + secondsToHms(timeLeft) +
"\r\nFinish: " + DateFormat(now, finishedTime);
} else {
timeLeftElement.textContent = "Will take: " + secondsToHms(timeLeft) + "\r\n Expected finished: " + DateFormat(now, finishedTime);
}
timeLeftElement.style.display = "block";
} else {
// empty and reset if no time
timeLeftElement.style.display = "none";
}
}
if (skillID != CONSTANTS.skill.Magic) {
// Generate progression Tooltips
if (!timeLeftElement._tippy) {
tippy(timeLeftElement, {
allowHTML: true,
interactive: false,
animation: false,
});
}
let wrapper = ['<div class="row"><div class="col-6" style="white-space: nowrap;"><h3 class="block-title m-1" style="color:white;" >','</h3></div><div class="col-6" style="white-space: nowrap;"><h3 class="block-title m-1 pl-1"><span class="p-1 bg-',' rounded" style="text-align:center; display: inline-block;line-height: normal;width: 70px;color:white;">','</span>','</h3></div></div>'];
let percentageSkill = (getPercentageInLevel(results.finalSkillXP,results.finalSkillXP,"skill")).toFixed(1);
let percentageSkillElement = (percentageSkill == 0) ? '' : ` +${percentageSkill}%`;
let finalSkillLevelElement = wrapper[0] + 'Final Skill Level ' + wrapper[1] + 'success' + wrapper[2] + convertXPToLvl(results.finalSkillXP,true) + ' / 99' + wrapper[3] + percentageSkillElement + wrapper[4];
let timeLeftSkillElement = '';
if (timeLeftSkill > 0){
let finishedTimeSkill = AddSecondsToDate(now,timeLeftSkill);
timeLeftSkillElement = '<div class="row"><div class="col-12 font-size-sm text-uppercase text-muted mb-1" style="text-align:center"><small style="display:inline-block;clear:both;white-space:pre-line;color:white;">Time to 99: ' + secondsToHms(timeLeftSkill) + '<br> Expected finished: ' + DateFormat(now,finishedTimeSkill) + '</small></div></div>';
}
let percentageMastery = (getPercentageInLevel(results.finalMasteryXP,results.finalMasteryXP,"mastery")).toFixed(1);
let percentageMasteryElement = (percentageMastery == 0) ? '' : ` +${percentageMastery}%`;
let finalMasteryLevelElement = wrapper[0] + 'Final Mastery Level ' + wrapper[1] + 'info' + wrapper[2] + convertXPToLvl(results.finalMasteryXP) + ' / 99' + wrapper[3] + percentageMasteryElement + wrapper[4];
let timeLeftMasteryElement = '';
if (timeLeftMastery > 0){
let finishedTimeMastery = AddSecondsToDate(now,timeLeftMastery);
timeLeftMasteryElement = '<div class="row"><div class="col-12 font-size-sm text-uppercase text-muted mb-1" style="text-align:center"><small style="display:inline-block;clear:both;white-space:pre-line;color:white;">Time to 99: ' + secondsToHms(timeLeftMastery) + '<br> Expected finished: ' + DateFormat(now,finishedTimeMastery) + '</small></div></div>';
}
let finalPoolPercentageElement = wrapper[0] + 'Final Mastery Pool ' + wrapper[1] + 'warning' + wrapper[2] + results.finalPoolPercentage + '%' + wrapper[3] + wrapper[4];
let timeLeftPoolElement = '';
if (timeLeftPool > 0){
let finishedTimePool = AddSecondsToDate(now,timeLeftPool);
timeLeftPoolElement = '<div class="row"><div class="col-12 font-size-sm text-uppercase text-muted mb-1" style="text-align:center"><small class="" style="display:inline-block;clear:both;white-space:pre-line;color:white;">Time to 100%: ' + secondsToHms(timeLeftPool) + '<br> Expected finished: ' + DateFormat(now,finishedTimePool) + '</small></div></div>';
}
let tooltip = '<div class="col-12 mt-1">' + finalSkillLevelElement + timeLeftSkillElement + finalMasteryLevelElement + timeLeftMasteryElement + finalPoolPercentageElement + timeLeftPoolElement +'</div>';
timeLeftElement._tippy.setContent(tooltip);
let poolProgress = (results.finalPoolPercentage > 100) ? 100 - ((initialTotalMasteryPoolXP / masteryPoolMaxXP)*100) : (results.finalPoolPercentage - ((initialTotalMasteryPoolXP / masteryPoolMaxXP)*100)).toFixed(4);
$(`#mastery-pool-progress-end-${skillID}`).css("width", poolProgress + "%");
let masteryProgress = getPercentageInLevel(initialTotalMasteryXP,results.finalMasteryXP,"mastery",true);
$(`#${skillID}-mastery-pool-progress-end`).css("width", masteryProgress + "%");
let skillProgress = getPercentageInLevel(initialSkillXP,results.finalSkillXP,"skill",true);
$(`#skill-progress-bar-end-${skillID}`).css("width", skillProgress + "%");
}
}
// select and start craft overrides
var selectRef = {};
var startRef = {};
[ // skill name, select names, < start name >
["Smithing", ["Smith"]],
["Fletching", ["Fletch"]],
["Runecrafting", ["Runecraft"]],
["Crafting", ["Craft"]],
["Herblore", ["Herblore"]],
["Cooking", ["Food"]],
["Firemaking", ["Log"], "burnLog"],
["Magic", ["Magic", "ItemForMagic"], "castMagic"],
].forEach(skill => {
let long = skill[0];
let shorts = skill[1];
let start = "start" + long;
if (skill.length > 2) {
start = skill[2];
}
// selects
shorts.forEach(short => {
selectRef[short] = window["select" + short];
window["select" + short] = function(...args) {
selectRef[short](...args);
try {
timeRemaining(CONSTANTS.skill[long]);
} catch (e) {
console.error(e);
}
};
});
// start
startRef[long] = window[start];
window[start] = function(...args) {
startRef[long](...args);
try {
timeRemaining(CONSTANTS.skill[long]);
taskComplete(CONSTANTS.skill[long]);
} catch (e) {
console.error(e);
}
};
});
}
function loadScript() {
if ((window.isLoaded && !window.currentlyCatchingUp) || (typeof unsafeWindow !== 'undefined' && unsafeWindow.isLoaded && !unsafeWindow.currentlyCatchingUp)) { // Only load script after game has opened
clearInterval(scriptLoader);
injectScript(script);
}
}
const scriptLoader = setInterval(loadScript, 200);
})();