Melvor TimeRemaining

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

当前为 2020-10-21 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Melvor TimeRemaining
// @namespace    http://tampermonkey.net/
// @version      0.5.0
// @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 and Visua#9999 for helping with parts of the code and troubleshooting


(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 send notifications
		function notify(msg) {
			One.helpers('notify', {
				type: 'dark',
				from: 'bottom',
				align: 'center',
				message: msg
			});
		}

		// Funtion to check if task is complete
		function taskComplete() {
			if (window.timeLeftLast > 1 && window.timeLeftCurrent === 0) {
				notify("Task Done");
				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]);

		// Funtion to get unformatted number for Qty
		function getQtyUnformat(itemID) {
			for (let i = 0; i < bank.length; i++) {
				if (bank[i].id === itemID) {
					return bank[i].qty;
				}
			}
			return 0;
		}

		// Convert seconds to hours/minutes/seconds and format them
		function secondsToHms(d) {
			d = Number(d);	
			let h = Math.floor(d / 3600);
			let m = Math.floor(d % 3600 / 60);
			let s = Math.floor(d % 3600 % 60);
			let sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : "";
			let mDisplay = m > 0 ? m + (m == 1 ? " minute" : " minutes") : "";
			let mDisplayComma = m > 0 && s > 0 ? " and " : "";
			let hDisplay = h > 0 ? h + (h == 1 ? " hour" : " hours") : "";
			let hDisplayComma = h > 0 ? ((m === 0 && s > 0)||(s === 0 && m > 0) ? " and " : ((s > 0 || m > 0) ? ", " : "")) : "";
			return hDisplay + hDisplayComma + mDisplay + mDisplayComma + sDisplay;
		}

		// Add seconds to date
		function AddSecondsToDate(date, seconds) {
			return new Date(date.getTime() + seconds*1000);
		}

		// Format date
		function DateFormat(date,time){
			let days = Math.floor(time / 86400);
			days = days > 0 ? ' + ' + days + ' days': '';
			let hours = date.getHours();
			hours = hours < 10 ? '0' + hours : hours;
			let minutes = date.getMinutes();
			minutes = minutes < 10 ? '0' + minutes : minutes;
			let strTime = hours + ':' + minutes + days;
			return strTime;
		}

		// 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 = exp.level_to_xp(level)+1;
			return xp;
		}

		// Convert XP value to level
		function convertXPToLvl(xp, noCap = false) {
			let level = exp.xp_to_level(xp) - 1;
			if (level < 1) level = 1;
			if(!noCap) {
				if (level > 99) level = 99;
			}
			return level;
		}

		// Main function
		function timeRemaining(currentSkill){
			// Reset variables
			var masteryID = 0;
			var skillInterval = 0; // Update interval of skill
			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(currentSkill); // 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,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 skillID = skillName.indexOf(currentSkill); // ID of skill
			var initialSkillXP = skillXP[skillID]; // Current skill XP
			
			// Set current skill and pull matching variables from game with script
			if (currentSkill == "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
			}
			if (currentSkill == "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 (selectedFletchLog === undefined) {selectedFletchLog = 0;}
					skillReq = [skillReq[selectedFletchLog]];
				}
			}
			if (currentSkill == "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 = [Infinity]; // Runecrafting has no Mastery bonus
				chanceToKeep = [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;
			}
			if (currentSkill == "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);
				}
			}
			if (currentSkill == "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);
				}
			}
			if (currentSkill == "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 = [Infinity]; //Cooking has no Mastery bonus
				chanceToKeep = [0]; //Thus no chance to keep
			}
			if (currentSkill == "Firemaking") {
				item = selectedLog;
				let bonfireBonus = logsData[selectedLog].bonfireBonus;
				itemXP = logsData[selectedLog].xp + logsData[selectedLog].xp * (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
			}

			if (currentSkill == "Magic") {
				skillInterval = 2000;
				//Find need runes for spell
				altMagicID = selectedAltMagic;
					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});
				}
				if (ALTMAGIC[selectedAltMagic].selectItem == -1) { // Spells that dont require you to select an item
					if (ALTMAGIC[selectedAltMagic].needCoal) { // Rags to Riches II
						skillReq.push({id: 48, qty: 1});
					}
				}
				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
			}

			// Configure initial mastery values for all skills with masteries
			if (currentSkill != "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) && currentSkill != "Magic") {
				for (let i = 0; i < masteryLimLevel.length; i++) {
					chanceToKeep[i] += 0.10; // Add base 10% chance
				}
				rhaelyxCharge = getQtyUnformat(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 resourse in Bank
				let itemQty = getQtyUnformat(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;

				if (currentSkill == "Smithing") {
					//none
				}
				if (currentSkill == "Fletching") {
					if (currentPoolMasteryXP >= poolLim[3]) adjustedInterval -= 200;
				}
				if (currentSkill == "Runecrafting") {
					//none
				}
				if (currentSkill == "Crafting") {
					//none
				}
				if (currentSkill == "Herblore") {
					//none
				}
				if (currentSkill == "Cooking") {
					//none
				}
				if (currentSkill == "Firemaking") {
					if (currentPoolMasteryXP >= poolLim[1]) adjustedInterval *= 0.9;
					let descreasedBurnInterval = 1 - convertXPToLvl(currentMasteryXP) * 0.001;
					adjustedInterval *= descreasedBurnInterval;
				}
				return adjustedInterval;
			}

			// Adjust preservation chance based on unlocked bonuses
			function preservationAdjustment(currentPoolMasteryXP) {

				let adjustedPreservation = 0;

				if (currentSkill == "Smithing") {
					if (currentPoolMasteryXP >= poolLim[1]) adjustedPreservation += 5;
					if (currentPoolMasteryXP >= poolLim[2]) adjustedPreservation += 5;

				}
				if (currentSkill == "Fletching") {
					//none
				}
				if (currentSkill == "Runecrafting") {
					if (currentPoolMasteryXP >= poolLim[2]) adjustedPreservation += 10;
				}
				if (currentSkill == "Crafting") {
					//none
				}
				if (currentSkill == "Herblore") {
					if (currentPoolMasteryXP >= poolLim[2]) adjustedPreservation += 5;
				}
				if (currentSkill == "Cooking") {
					if (currentPoolMasteryXP >= poolLim[2]) adjustedPreservation += 10;
				}
				if (currentSkill == "Firemaking") {

				}
				return adjustedPreservation / 100;
			}

			// Adjust skill XP based on unlocked bonuses
			function skillXPAdjustment(currentPoolMasteryXP, currentMasteryXP) {

				let xpMultiplier = 1;

				if (currentSkill == "Smithing") {
					//none
				}
				if (currentSkill == "Fletching") {
					//none
				}
				if (currentSkill == "Runecrafting") {
					if (currentPoolMasteryXP >= poolLim[1]) xpMultiplier += 1.5;
				}
				if (currentSkill == "Crafting") {
					//none
				}
				if (currentSkill == "Herblore") {
					//if (currentPoolMasteryXP >= poolLim[1]) xpMultiplier += 0.03;
				}
				if (currentSkill == "Cooking") {
					let burnchance = calcburnChance(currentMasteryXP);
					let cookXP = itemXP * (1 - burnchance);
					let burnXP = 1 * burnchance;
					return cookXP + burnXP;
				}
				if (currentSkill == "Firemaking") {
					// none
				}
				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;
				let xpToAdd = (((calcTotalUnlockedItems(skillID, currentTotalSkillXP) * currentTotalMasteryLevelForSkill) / getTotalMasteryLevelForSkill(skillID) + convertXPToLvl(currentMasteryXP) * (getTotalItemsInSkill(skillID) / 10)) * (timePerAction / 1000)) / 2; // General Mastery XP formula
				if (currentPoolMasteryXP >= poolLim[0]) xpModifier += 0.05;
				// If current skill is Firemaking, we need to apply mastery progression from actions and use updated currentPoolMasteryXP values
				if (currentSkill == "Firemaking") {
					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;
				}
				// For all other skills, you use the game function to grab your FM mastery progression
				else {
					if (getMasteryPoolProgress(CONSTANTS.skill.Firemaking) >= poolLim[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;
					}
				}

				if (petUnlocked[21]) xpModifier += 0.03;
				if (equippedItems.includes(CONSTANTS.item.Ancient_Ring_Of_Mastery)) xpModifier += items[CONSTANTS.item.Ancient_Ring_Of_Mastery].bonusMasteryXP;
				xpToAdd *= xpModifier;
				if (xpToAdd < 1) xpToAdd = 1;
				
				// Burnchance affects mastery XP
				if (currentSkill == "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;
			}

			// adjusting totalChanceToUse base on Rhaelyx charges and number of actions
			function adjustTotalChanceToUseRhaelyx(totalChanceToUse, expectedActions, resources) {
				if (chargeUses >= expectedActions || chargeUses >= resources / (totalChanceToUse - RhaelyxChance)) {
					return totalChanceToUse - RhaelyxChance; //if we have excess uses, then we simply use better chance to keep and move on as usual
				} else {
					return resources / (chargeUses / (totalChanceToUse - RhaelyxChance) + (resources - chargeUses) / totalChanceToUse); //the denominator is the "real" expectedXP with Rhaelyx, so the "real" chanceToKeep is essentially found through resources/(resources/chanceToKeep)
				}
			}

			// Calculates expected time, taking into account Mastery Level advancements during the craft
			function calcExpectedTime(resources){
				let sumTime = 0;
				let currentTotalMasteryXP = initialTotalMasteryXP;
				let currentTotalSkillXP = initialSkillXP;
				let currentTotalPoolXP = initialTotalMasteryPoolXP;
				let currentTotalMasteryLevelForSkill = initialTotalMasteryLevelForSkill;
				
				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, currentMasteryXP);
					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;
					let resourceActions = Math.round(resources / adjustTotalChanceToUseRhaelyx(totalChanceToUse, resources, resources));
					
					// Mininum 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 {
						resources -= Math.round(expectedActions*adjustTotalChanceToUseRhaelyx(totalChanceToUse, expectedActions, resources));
					}
					
					// Update time and XP
					sumTime += expectedActions * currentInterval;
					currentTotalMasteryXP += currentMasteryXP*expectedActions;
					currentTotalSkillXP += currentSkillXP*expectedActions;
					currentTotalPoolXP += currentPoolXP*expectedActions;

					// Update remaining Rhaelyx Charge uses
					chargeUses -= expectedActions;
					if ( chargeUses < 0 ) chargeUses = 0;
					
					// Level up mastery if hitting Mastery limit 
					if ( masteryXPActions == expectedActions ) currentTotalMasteryLevelForSkill++;
				}
				return {"timeLeft" : Math.round(sumTime), "finalSkillLevel" : convertXPToLvl(currentTotalSkillXP,true), "finalMasteryLevel" : convertXPToLvl(currentTotalMasteryXP), "finalPoolPercentage" : Math.min((currentTotalPoolXP/masteryPoolMaxXP)*100,100).toFixed(2)};
			}

			var results = calcExpectedTime(recordCraft);
			
			//Time left
			var timeLeft = 0;
			if (currentSkill == "Magic") {
				timeLeft = Math.round(recordCraft * skillInterval / 1000);
			} else {
				timeLeft = Math.round(results.timeLeft / 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);
					timeLeftElement.textContent = "Will take: " + secondsToHms(timeLeft) + "\r\n Expected finished: " + DateFormat(finishedTime,timeLeft);
					timeLeftElement.style.display = "block";
				} else {
				// empty and reset if no time
					timeLeftElement.style.display = "none";
				}
			}
			// Generate progression Tooltips for all skills that aren't Magic
			if (currentSkill != "Magic") {
				if (!timeLeftElement._tippy) {
					tippy(timeLeftElement, {
						allowHTML: true,
						interactive: false,
						animation: false,
					  });
				};
				let lightMode = "";
				if (!darkMode) lightMode = ' style="color:white;"';
				let wrapper = ['<div class="row"><div class="col-8" style="white-space: nowrap;"><h3 class="block-title m-1"' + lightMode + ' >','</h3></div><div class="col-4" style="white-space: nowrap; text-align:right;"><h3 class="block-title m-1"' + lightMode + '><span class="p-1 bg-',' rounded" style="text-align:center; display: inline-block;line-height: normal;width: 70px;">','</span></h3></div></div>'];
				let finalSkillLevel = wrapper[0] + 'Final Skill Level ' + wrapper[1] + 'success' + wrapper[2] + results.finalSkillLevel + ' / 99' + wrapper[3];
				let finalMasteryLevel = wrapper[0] + 'Final Mastery Level ' + wrapper[1] + 'info' + wrapper[2] + results.finalMasteryLevel + ' / 99' + wrapper[3];
				let finalPoolPercentage = wrapper[0] + 'Final Mastery Pool ' + wrapper[1] + 'warning' + wrapper[2] + results.finalPoolPercentage + '%' + wrapper[3];
				let tooltip = '<div class="col-12 mt-1">' + finalSkillLevel + finalMasteryLevel + finalPoolPercentage + '</div>';
				timeLeftElement._tippy.setContent(tooltip);
			}
		}

		// ## SMITHING ##
		var selectSmithRef = selectSmith;
		window.selectSmith = function(...args) {
			selectSmithRef(...args);
			try {
				timeRemaining("Smithing");
			} catch (e) {
				console.error(e);
			}
		};
		var startSmithingRef = startSmithing;
		window.startSmithing = function(...args) {
			startSmithingRef(...args);
			try {
				timeRemaining("Smithing");
				taskComplete();
			} catch (e) {
				console.error(e);
			}
		};

		// ## FLETCHING ##
		var selectFletchRef = selectFletch;
		window.selectFletch = function(...args) {
			selectFletchRef(...args);
			try {
				timeRemaining("Fletching");
			} catch (e) {
				console.error(e);
			}
		};
		var startFletchingRef = startFletching;
		window.startFletching = function(...args) {
			startFletchingRef(...args);
			try {
				timeRemaining("Fletching");
				taskComplete();
			} catch (e) {
				console.error(e);
			}
		};

		// ## RUNECRAFTING ##
		var selectRunecraftRef = selectRunecraft;
		window.selectRunecraft = function(...args) {
			selectRunecraftRef(...args);
			try {
				timeRemaining("Runecrafting");
			} catch (e) {
				console.error(e);
			}
		};
		var startRunecraftingRef = startRunecrafting;
		window.startRunecrafting = function(...args) {
			startRunecraftingRef(...args);
			try {
				timeRemaining("Runecrafting");
				taskComplete();
			} catch (e) {
				console.error(e);
			}
		};

		// ## CRAFTING ##
		var selectCraftRef = selectCraft;
		window.selectCraft = function(...args) {
			selectCraftRef(...args);
			try {
				timeRemaining("Crafting");
			} catch (e) {
				console.error(e);
			}
		};
		var startCraftingRef = startCrafting;
		window.startCrafting = function(...args) {
			startCraftingRef(...args);
			try {
				timeRemaining("Crafting");
				taskComplete();
			} catch (e) {
				console.error(e);
			}
		};

		// ## HERBLORE ##
		var selectHerbloreRef = selectHerblore;
		window.selectHerblore = function(...args) {
			selectHerbloreRef(...args);
			try {
				timeRemaining("Herblore");
			} catch (e) {
				console.error(e);
			}
		};
		var startHerbloreRef = startHerblore;
		window.startHerblore = function(...args) {
			startHerbloreRef(...args);
			try {
				timeRemaining("Herblore");
				taskComplete();
			} catch (e) {
				console.error(e);
			}
		};

		// ## COOKING ##
		var selectFoodRef = selectFood;
		window.selectFood = function(...args) {
			selectFoodRef(...args);
			try {
				timeRemaining("Cooking");
			} catch (e) {
				console.error(e);
			}
		};
		var startCookingRef = startCooking;
		window.startCooking = function(...args) {
			startCookingRef(...args);
			try {
				timeRemaining("Cooking");
				taskComplete();
			} catch (e) {
				console.error(e);
			}
		};

		// ## FIREMAKING ##
		var selectLogRef = selectLog;
		window.selectLog = function(...args) {
			selectLogRef(...args);
			try {
				timeRemaining("Firemaking");
			} catch (e) {
				console.error(e);
			}
		};
		var burnLogRef = burnLog;
		window.burnLog = function(...args) {
			burnLogRef(...args);
			try {
				timeRemaining("Firemaking");
				taskComplete();
			} catch (e) {
				console.error(e);
			}
		};

		// ## ALT MAGIC ##
		var selectMagicRef = selectMagic;
		window.selectMagic = function(...args) {
			selectMagicRef(...args);
			try {
				timeRemaining("Magic");
			} catch (e) {
				console.error(e);
			}
		};
		var selectItemForMagicRef = selectItemForMagic;
		window.selectItemForMagic = function(...args) {
			selectItemForMagicRef(...args);
			try {
				timeRemaining("Magic");
			} catch (e) {
				console.error(e);
			}
		};
		var castMagicRef = castMagic;
		window.castMagic = function(...args) {
			castMagicRef(...args);
			try {
				timeRemaining("Magic");
				taskComplete();
			} 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);
})();