Melvor TimeRemaining

Shows time remaining for completing a task with your current resources. Takes into account Mastery Levels and other bonuses.

Instalar este script¿?
Script recomendado por el autor

Puede que también te guste Melvor ETA.

Instalar este script
// ==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);
})();