// ==UserScript==
// @name Melvor Action Queue
// @version 0.2.7
// @description Adds an interface to queue up actions based on triggers you set
// @author 8992
// @match https://*.melvoridle.com/*
// @grant none
// @namespace http://tampermonkey.net/
// @noframes
// ==/UserScript==
let isVisible = false;
let currentActionIndex = 0;
let triggerCheckInterval = null;
let nameIncrement = 0;
let queueLoop = false;
let queuePause = false;
let manageMasteryInterval = null;
const actionQueueArray = [];
const shop = {};
const tooltips = {};
const validInputs = {
A: [null, null, null],
B: [null, null, null, null],
C: [null, null, null, null],
};
let currentlyEditing = { id: null, type: null };
function getBankQty(id) {
const item = bank.find((a) => a.id == id);
return item ? item.qty : 0;
}
function checkAmmoQty(id) {
const set = equipmentSets.find((a) => a.equipment[CONSTANTS.equipmentSlot.Quiver] == id);
return set ? set.ammo : 0;
}
function checkFoodQty(id) {
const food = equippedFood.find((a) => a.itemID == id);
return food ? food.qty : 0;
}
function actionTab() {
if (!isVisible) {
changePage(3);
$("#settings-container").attr("class", "content d-none");
$("#header-title").text("Action Queue");
$("#header-icon").attr("src", "assets/media/skills/prayer/mystic_lore.svg");
$("#header-theme").attr("class", "content-header bg-combat");
$("#page-header").attr("class", "bg-combat");
document.getElementById("action-queue-container").style.display = "";
isVisible = true;
}
}
function hideActionTab() {
if (isVisible) {
document.getElementById("action-queue-container").style.display = "none";
isVisible = false;
}
}
const options = {
//trigger options for dropdown menus
triggers: {
"Item Quantity": {},
"Skill Level": {},
"Skill XP": {},
"Equipped Item Quantity": {},
"Pet Unlocked": {},
"Mastery Pool %": {},
"Mastery Level": {
Cooking: {},
Crafting: {},
Farming: {},
Firemaking: {},
Fishing: {},
Fletching: {},
Herblore: {},
Mining: {},
Runecrafting: {},
Smithing: {},
Thieving: {},
Woodcutting: {},
},
},
//action options for dropdown menus
actions: {
"Start Skill": {
Cooking: {},
Crafting: {},
Firemaking: {},
Fishing: {},
Fletching: {},
Herblore: {},
Magic: {},
Mining: {},
Runecrafting: {},
Smithing: {},
Thieving: {},
Woodcutting: {},
},
"Start Combat": {},
"Switch Equipment Set": { 1: null, 2: null, 3: null },
"Equip Item": {},
"Unequip Item": {},
"Buy Item": {},
"Sell Item": {},
},
};
class ShopItem {
/**
* Creates a shop item
* @param {number} gp gp cost
* @param {number} slayerCoins slayer coins cost
* @param {function} req function that checks non-price requirements and returns boolean
* @param {function} buy function that buys the item with qty as parameter where applicable
*/
constructor(gp, slayerCoins, req, buy) {
this.gp = gp;
this.slayerCoins = slayerCoins;
this.req = req;
this.buy = buy;
}
}
ShopItem.prototype.cost = function () {
return gp >= this.gp && slayerCoins >= this.slayerCoins && baseBankMax + bankMax > bank.length;
};
function setTrigger(category, name, greaterThan, masteryItem, number) {
const itemID = items.findIndex((a) => a.name == name);
number = parseInt(number, 10);
switch (category) {
case "Item Quantity":
if (greaterThan == "≥") {
return () => {
return getBankQty(itemID) >= number;
};
}
return () => {
return getBankQty(itemID) <= number;
};
case "Skill Level": {
const xp = exp.level_to_xp(number);
return () => {
return skillXP[CONSTANTS.skill[name]] >= xp;
};
}
case "Skill XP":
return () => {
return skillXP[CONSTANTS.skill[name]] >= number;
};
case "Equipped Item Quantity":
if (items.filter((a) => a.canEat).find((a) => a.name == name)) {
return () => {
return checkFoodQty(itemID) <= number;
};
}
return () => {
return checkAmmoQty(itemID) <= number;
};
case "Mastery Level":
return setMasteryTrigger(name, number, masteryItem);
case "Pet Unlocked": {
const petID = PETS.findIndex((pet) => {
const re = new RegExp(`^${name.split(" (")[0]}`, "g");
return re.test(pet.name);
});
return () => {
return petUnlocked[petID];
};
}
case "Mastery Pool %": {
const skill = CONSTANTS.skill[name];
return () => {
return getMasteryPoolProgress(skill) >= number;
};
}
}
}
function setMasteryTrigger(name, number, masteryItem) {
const itemID = items.findIndex((a) => a.name == masteryItem);
let masteryID = 0;
switch (name) {
case "Cooking":
masteryID = cookingItems.find((a) => a.itemID == itemID).cookingID;
break;
case "Crafting":
masteryID = items[itemID].masteryID[1];
break;
case "Farming":
masteryID = items[itemID].masteryID[1];
break;
case "Firemaking":
masteryID = itemID;
break;
case "Fishing":
masteryID = fishingItems.find((a) => a.itemID == itemID).fishingID;
break;
case "Fletching":
masteryID = fletchingItems.find((a) => a.itemID == itemID).fletchingID;
break;
case "Herblore":
masteryID = herbloreItemData.findIndex((a) => a.name == masteryItem);
break;
case "Mining":
masteryID = miningData.find((a) => a.ore == itemID).masteryID;
break;
case "Runecrafting":
masteryID = runecraftingItems.find((a) => a.itemID == itemID).runecraftingID;
break;
case "Smithing":
masteryID = smithingItems.find((a) => a.itemID == itemID).smithingID;
break;
case "Thieving":
masteryID = thievingNPC.findIndex((a) => a.name == masteryItem);
break;
case "Woodcutting":
masteryID = itemID;
}
return () => {
return getMasteryLevel(CONSTANTS.skill[name], masteryID) >= number;
};
}
function setAction(actionCategory, actionName, skillItem, skillItem2, qty) {
qty = parseInt(qty, 10);
const itemID = items.findIndex((a) => a.name == actionName);
switch (actionCategory) {
case "Start Skill":
return setSkillAction(actionName, skillItem, skillItem2);
case "Start Combat": {
const dungeonIndex = DUNGEONS.findIndex((a) => a.name == actionName);
const monsterIndex = MONSTERS.findIndex((a) => a.name == actionName);
return () => {
if (dungeonIndex >= 0) {
if (
DUNGEONS[dungeonIndex].requiresCompletion === undefined ||
dungeonCompleteCount[DUNGEONS[dungeonIndex].requiresCompletion] >= 1
) {
selectDungeon(dungeonIndex, true);
return true;
}
}
for (const area of combatAreas) {
if (area.monsters.includes(monsterIndex)) {
selectMonster(monsterIndex);
return true;
}
}
for (const area of slayerAreas) {
if (skillLevel[CONSTANTS.skill.Slayer] >= area.slayerLevel && area.monsters.includes(monsterIndex)) {
selectMonster(monsterIndex);
return true;
}
}
return false;
};
}
case "Switch Equipment Set": {
const equipSet = parseInt(actionName) - 1;
return () => {
if (equipmentSetCount >= equipSet) {
setEquipmentSet(equipSet);
return true;
}
return false;
};
}
case "Equip Item":
return () => {
const bankID = getBankId(itemID);
if (bankID === false) return false;
if (sellItemMode) toggleSellItemMode();
if (items[itemID].canEat) {
selectBankItem(itemID);
equipFoodQty = bank[bankID].qty;
equipFood();
return getBankQty(itemID) == 0; //returns false if there is any of the item left in bank (couldn't equip)
}
if (items[itemID].equipmentSlot == CONSTANTS.equipmentSlot.Quiver) {
equipItem(itemID, bank[bankID].qty, selectedEquipmentSet);
return getBankQty(itemID) == 0; //returns false if there is any of the item left in bank (couldn't equip)
}
equipItem(itemID, 1, selectedEquipmentSet);
return equippedItems[items[itemID].equipmentSlot] === itemID; //returns false if the item is not equipped
};
case "Unequip Item": {
return () => {
const i = equippedItems.findIndex((a) => a == itemID);
if (i >= 0) {
unequipItem(i);
return getBankQty(itemID) > 0; //returns false if there is 0 of the items in bank (no space to unequip)
}
return false;
};
}
case "Buy Item":
return () => {
if (!shop[actionName].req(qty) || !shop[actionName].cost()) return false;
shop[actionName].buy(qty);
return true;
};
case "Sell Item":
return () => {
if (!bank.find((a) => a.id == itemID)) return false;
if (sellItemMode) toggleSellItemMode();
selectBankItem(itemID);
sellItem();
if (showSaleNotifications) swal.clickConfirm();
return true;
};
}
}
function setSkillAction(actionName, skillItem, skillItem2) {
const itemID = items.findIndex((a) => a.name == skillItem);
let actionID = 0;
switch (actionName) {
case "Cooking":
actionID = cookingItems.find((a) => a.itemID == itemID).cookingID;
return () => {
if (skillLevel[CONSTANTS.skill.Cooking] < items[itemID].cookingLevel) return false;
if (selectedFood !== itemID) selectFood(itemID);
if (!isCooking) startCooking(0, false);
return true;
};
case "Crafting":
actionID = craftingItems.find((a) => a.itemID == itemID).craftingID;
return () => {
if (skillLevel[CONSTANTS.skill.Crafting] < craftingItems[actionID].craftingLevel) return false;
if (selectedCraft !== actionID) selectCraft(actionID);
if (!isCrafting) startCrafting(true);
return true;
};
case "Firemaking":
actionID = items[itemID].firemakingID;
return () => {
if (skillLevel[CONSTANTS.skill.Firemaking] < logsData[actionID].level) return false;
if (selectedLog !== actionID) selectLog(actionID);
if (isBurning) burnLog(false);
if (!isBurning) burnLog(false);
return true;
};
case "Fishing": {
const fishIndex = fishingItems.find((a) => a.itemID == itemID).fishingID;
const areaID = fishingAreas.findIndex((a) => a.fish.includes(fishIndex));
const fishID = fishingAreas[areaID].fish.findIndex((a) => a == fishIndex);
return () => {
if (
(!equippedItems.includes(CONSTANTS.item.Barbarian_Gloves) && areaID == 6) ||
(!secretAreaUnlocked && areaID == 7) ||
skillLevel[CONSTANTS.skill.Fishing] < fishingItems[fishIndex].fishingLevel
)
return false;
if (!isFishing) {
selectFish(areaID, fishID);
startFishing(areaID, fishID, true);
} else {
if (areaID != offline.action[0] || fishID != offline.action[1]) {
startFishing(offline.action[0], offline.action[1], true);
selectFish(areaID, fishID);
startFishing(areaID, fishID, true);
}
}
return true;
};
}
case "Fletching":
actionID = fletchingItems.find((a) => a.itemID == itemID).fletchingID;
return () => {
if (skillLevel[CONSTANTS.skill.Fletching] < fletchingItems[actionID].fletchingLevel) return false;
if (selectedFletch !== actionID) selectFletch(actionID);
if (!isFletching) startFletching(true);
return true;
};
case "Herblore":
actionID = herbloreItemData.findIndex((a) => a.name == skillItem);
return () => {
if (skillLevel[CONSTANTS.skill.Herblore] < herbloreItemData[actionID].herbloreLevel) return false;
if (selectedHerblore !== actionID) selectHerblore(actionID);
if (!isHerblore) startHerblore(true);
return true;
};
case "Mining":
actionID = miningData.findIndex((a) => a.ore == itemID);
return () => {
if (
(actionID === 9 && !canMineDragonite()) ||
skillLevel[CONSTANTS.skill.Mining] < miningData[actionID].level ||
rockData[actionID].depleted
)
return false;
if (!isMining || currentRock != actionID) mineRock(actionID, true);
return true;
};
case "Magic": {
actionID = ALTMAGIC.findIndex((a) => a.name == skillItem);
const magicItem = items.findIndex((a) => a.name == skillItem2);
return () => {
if (
skillLevel[CONSTANTS.skill.Magic] < ALTMAGIC[actionID].magicLevelRequired ||
skillLevel[CONSTANTS.skill.Smithing] < smithingItems.find((a) => a.smithingLevel == skillItem2)
)
return false;
if (selectedAltMagic !== actionID) selectMagic(actionID);
if (ALTMAGIC[actionID].selectItem >= 0 && selectedMagicItem[ALTMAGIC[actionID].selectItem] !== magicItem) {
if (getBankQty(magicItem) < 1 || lockedItems.includes(magicItem)) return false;
selectItemForMagic(ALTMAGIC[actionID].selectItem, magicItem, false);
}
if (!isMagic) castMagic(true);
return true;
};
}
case "Runecrafting":
actionID = runecraftingItems.findIndex((a) => a.itemID == itemID);
return () => {
if (skillLevel[CONSTANTS.skill.Runecrafting] < runecraftingItems[actionID].runecraftingLevel) return false;
if (selectedRunecraft !== actionID) selectRunecraft(actionID);
if (!isRunecrafting) startRunecrafting(true);
return true;
};
case "Smithing":
actionID = smithingItems.findIndex((a) => a.itemID == itemID);
return () => {
if (skillLevel[CONSTANTS.skill.Smithing] < smithingItems[actionID].smithingLevel) return false;
if (selectedSmith !== actionID) selectSmith(actionID);
if (!isSmithing) startSmithing(true);
return true;
};
case "Thieving":
actionID = thievingNPC.findIndex((a) => a.name == skillItem);
return () => {
if (skillLevel[CONSTANTS.skill.Thieving] < thievingNPC[actionID].level) return false;
if (isThieving) pickpocket(npcID);
pickpocket(actionID);
return true;
};
case "Woodcutting":
actionID = [itemID, items.findIndex((a) => a.name == skillItem2)];
return () => {
let result = true;
treeCuttingHandler.forEach((tree, i) => {
if (tree !== null && !actionID.slice(0, treeCutLimit).includes(i)) cutTree(i);
});
for (let i = 0; i < treeCutLimit; i++) {
if (treeCuttingHandler[actionID[i]] === null) {
if (skillLevel[CONSTANTS.skill.Woodcutting] >= trees[actionID[i]].level) {
cutTree(actionID[i]);
} else {
result = false;
}
}
}
return result;
};
}
}
class Action {
/**
* Create an action object with trigger
* @param {string} category (Tier 1 option) category for trigger
* @param {string} name (Tier 2 option) skill/item name
* @param {string} greaterThan (Tier 3 option) either ≥ or ≤
* @param {string} masteryItem (Tier 3 option) target name for mastery
* @param {string} number (Tier 4 option) target number for skill/mastery/item
* @param {string} actionCategory (Tier 1 option) category for action
* @param {string} actionName (Tier 2 option) skill/monster/item/set name
* @param {string} skillItem (Tier 3 option) name for skilling action
* @param {string} skillItem2 (Tier 4 option) name of second tree to cut or alt.magic item
* @param {string} qty (Tier 4 option) amount of item to buy if applicable
*/
constructor(category, name, greaterThan, masteryItem, number, actionCategory, actionName, skillItem, skillItem2, qty) {
switch (category) {
case "Item Quantity":
this.description = `If ${name} ${greaterThan} ${number}:`;
break;
case "Skill Level":
this.description = `If ${name} ≥ level ${number}:`;
break;
case "Skill XP":
this.description = `If ${name} ≥ ${number}xp:`;
break;
case "Equipped Item Quantity": {
let plural = name;
if (!/s$/i.test(name)) plural += "s";
this.description = `If ≤ ${number} ${plural} equipped:`;
break;
}
case "Mastery Level":
this.description = `If ${name} ${masteryItem} mastery ≥ ${number}:`;
break;
case "Pet Unlocked":
this.description = `If ${name} unlocked:`;
break;
case "Mastery Pool %":
this.description = `If ${name} mastery pool ≥ ${number}%:`;
}
this.data = [category, name, greaterThan, masteryItem, number];
this.elementID = `AQ${nameIncrement++}`;
this.trigger = setTrigger(category, name, greaterThan, masteryItem, number);
if (typeof actionCategory == "string") {
this.action = [
{
elementID: `AQ${nameIncrement++}`,
data: [actionCategory, actionName, skillItem, skillItem2, qty],
start: setAction(actionCategory, actionName, skillItem, skillItem2, qty),
description: actionDescription(actionCategory, actionName, skillItem, skillItem2, qty),
},
];
} else {
this.action = [];
}
}
}
function actionDescription(actionCategory, actionName, skillItem, skillItem2, qty) {
let description = "";
switch (actionCategory) {
case "Start Skill":
description += `start ${actionName} ${skillItem}`;
if (actionName == "Woodcutting") description += ` & ${skillItem2}`;
if (actionName == "Magic") {
description += ` with ${skillItem2}`;
if (!/s$/i.test(skillItem2)) description += "s";
}
break;
case "Start Combat":
description += `start fighting ${actionName}`;
break;
case "Switch Equipment Set":
description += `switch to equipment set ${actionName}`;
break;
case "Equip Item":
description += `equip ${actionName} to current set`;
break;
case "Unequip Item":
description += `unequip ${actionName} from current set`;
break;
case "Buy Item":
if (qty > 1) {
description += `buy ${qty} ${actionName} from shop`;
} else {
description += `buy ${actionName} from shop`;
}
break;
case "Sell Item":
description += `sell ${actionName}`;
}
return description;
}
function resetForm(arr) {
arr.forEach((menu) => {
document.getElementById(`aq-num${menu}`).type = "hidden";
document.getElementById(`aq-num${menu}`).value = "";
validInputs[menu].forEach((a, i) => {
if (i != 0) {
document.getElementById(`aq-text${menu}${i}`).type = "hidden"; //hide all except first
document.getElementById(`aq-list${menu}${i}`).innerHTML = ""; //empty datalists
}
document.getElementById(`aq-text${menu}${i}`).value = ""; //clear values
validInputs[menu][i] = null;
});
});
}
function submitForm() {
try {
//create array of the input values
const arr = [];
["A", "B"].forEach((menu) => {
for (let i = 0; i < validInputs[menu].length; i++) arr.push(htmlChar(document.getElementById(`aq-text${menu}${i}`).value));
arr.push(document.getElementById(`aq-num${menu}`).value);
});
arr.splice(["≥", "≤", ""].includes(arr[2]) ? 3 : 2, 0, "");
//validate trigger and action
if (
validateInput(
options.triggers,
arr.slice(0, 5).filter((a) => a !== "")
) &&
validateInput(
options.actions,
arr.slice(5).filter((a) => a !== "")
)
) {
addToQueue(new Action(...arr));
resetForm(["A", "B"]);
}
} catch (e) {
console.error(e);
}
return false;
}
/**
* Function to validate user input
* @param {object} obj options object to check against
* @param {array} tier array of user inputs
* @param {number} n leave blank (used for recursion)
* @returns {boolean} true if input is valid
*/
function validateInput(obj, tier, n = 0) {
if (!obj.hasOwnProperty([tier[n]])) return false;
if (obj[tier[n]] === null || (obj[tier[n]] === "num" && /^\d{1,10}$/.test(tier[n + 1]))) return true;
return validateInput(obj[tier[n]], tier, n + 1);
}
/**
* Updates input text boxes
* @param {string} menu ('A'||'B'||'C')
*/
function dropdowns(menu) {
let obj;
if (menu == "A") {
obj = options.triggers;
} else if (menu == "B") {
obj = options.actions;
} else {
obj = options[currentlyEditing.type == "triggers" ? "triggers" : "actions"];
}
for (let i = 0; i < validInputs[menu].length; i++) {
const value = htmlChar(document.getElementById(`aq-text${menu}${i}`).value);
if (Object.keys(obj).includes(value)) {
if (obj[value] == "num") {
document.getElementById(`aq-num${menu}`).type = "text";
break;
}
if (obj[value] == null) break;
obj = obj[value];
if (validInputs[menu][i] != value) {
validInputs[menu][i] = value;
document.getElementById(`aq-text${menu}${i + 1}`).type = "text";
document.getElementById(`aq-list${menu}${i + 1}`).innerHTML = "";
Object.keys(obj).forEach((e) => {
document.getElementById(`aq-list${menu}${i + 1}`).insertAdjacentHTML("beforeend", `<option>${e}</option>`);
});
}
} else {
for (i++; i < validInputs[menu].length; i++) {
try {
document.getElementById(`aq-text${menu}${i}`).type = "hidden";
document.getElementById(`aq-text${menu}${i}`).value = "";
} catch {}
}
document.getElementById(`aq-num${menu}`).type = "hidden";
document.getElementById(`aq-num${menu}`).value = "";
}
}
}
const aqHTML = `<div class="content" id="action-queue-container" style="display: none">
<div class="row row-deck">
<div class="col-md-12">
<div class="block block-rounded block-link-pop border-top border-settings border-4x">
<form class="aq-popup" id="aq-edit-container" style="display: none;">
<div style="display: inline-block; margin-left: 20px; height: 180px; vertical-align: top">
<h3 id="aq-edit-form" class="aq-header">Action</h3>
<div>
<input type="text" class="aq-dropdown" id="aq-textC0" required list="aq-listC0" placeholder="Category" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-textC1" list="aq-listC1" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-textC2" list="aq-listC2" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-textC3" list="aq-listC3" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-numC" pattern="^\\d{1,10}$" placeholder="number" title="Positive integer"/>
</div>
</div>
<div style="margin-left: 20px">
<button type="button" id="aq-save-edit" class="btn btn-sm aq-blue">Save</button>
<button type="button" id="aq-cancel" class="btn btn-sm btn-danger">Cancel</button>
</div>
</form>
<div class="block-content">
<div>
<form id="aq-form">
<div style="display: inline-block; margin-left: 20px; height: 180px; vertical-align: top">
<h3 class="aq-header">Trigger</h3>
<div>
<input type="text" class="aq-dropdown" id="aq-textA0" required list="aq-listA0" placeholder="Category" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-textA1" list="aq-listA1" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-textA2" list="aq-listA2" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-numA" pattern="^\\d{1,10}$" placeholder="number" title="Positive integer"/>
</div>
</div>
<div style="display: inline-block; margin-left: 20px; height: 180px; vertical-align: top">
<h3 class="aq-header">Action</h3>
<div>
<input type="text" class="aq-dropdown" id="aq-textB0" required list="aq-listB0" placeholder="Category" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-textB1" list="aq-listB1" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-textB2" list="aq-listB2" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-textB3" list="aq-listB3" />
</div>
<div>
<input type="hidden" class="aq-dropdown" id="aq-numB" pattern="^\\d{1,10}$" placeholder="number" title="Positive integer"/>
</div>
</div>
<div style="margin-left: 20px">
<input type="submit" class="btn btn-sm aq-blue" value="Add to queue">
<button type="button" id="aq-pause" class="btn btn-sm aq-yellow">Pause</button>
</div>
</form>
</div>
<form style="margin: 10px 0 5px 20px">
<button type="button" class="btn btn-sm aq-grey" id="aq-download">Download Action List</button>
<input type="submit" class="btn btn-sm aq-grey" value="Import Action List" />
<input type="text" id="aq-pastebin" style="width: 236px;" required pattern="^\\[.*\\]$" placeholder="Paste data here" />
</form>
<div style="display: flex;justify-content: space-between;max-width: 550px;margin: 10px 0 0 25px;">
<p style="margin: 0;">Looping</p>
<div class="">
<div class="custom-control custom-radio custom-control-inline custom-control-lg">
<input type="radio" class="custom-control-input" id="aq-loop-enable" name="aq-looping">
<label class="custom-control-label" for="aq-loop-enable">Enable</label>
</div>
<div class="custom-control custom-radio custom-control-inline custom-control-lg">
<input type="radio" class="custom-control-input" id="aq-loop-disable" name="aq-looping" checked="">
<label class="custom-control-label" for="aq-loop-disable">Disable</label>
</div>
</div>
</div>
<div style="display: flex;justify-content: space-between;max-width: 550px;margin: 10px 0 0 25px;">
<p style="margin: 0;">Mastery Pool Management</p>
<div class="">
<div class="custom-control custom-radio custom-control-inline custom-control-lg">
<input type="radio" class="custom-control-input" id="aq-mastery-enable" name="aq-mastery">
<label class="custom-control-label" for="aq-mastery-enable">Enable</label>
</div>
<div class="custom-control custom-radio custom-control-inline custom-control-lg">
<input type="radio" class="custom-control-input" id="aq-mastery-disable" name="aq-mastery" checked="">
<label class="custom-control-label" for="aq-mastery-disable">Disable</label>
</div>
</div>
</div>
<h2 class="content-heading border-bottom mb-4 pb-2">Current Queue</h2>
<div style="min-height: 50px" id="aq-item-container"></div>
</div>
</div>
</div>
</div>
</div>
<datalist id="aq-listA0"></datalist>
<datalist id="aq-listA1"></datalist>
<datalist id="aq-listA2"></datalist>
<datalist id="aq-listB0"></datalist>
<datalist id="aq-listB1"></datalist>
<datalist id="aq-listB2"></datalist>
<datalist id="aq-listB3"></datalist>
<datalist id="aq-listC0"></datalist>
<datalist id="aq-listC1"></datalist>
<datalist id="aq-listC2"></datalist>
<datalist id="aq-listC3"></datalist>
<style>
.aq-dropdown {
width: 260px;
}
.aq-header {
margin-bottom: 10px;
color: whitesmoke;
}
.aq-arrow {
font-size: 2rem;
padding: 0px 0.25rem;
line-height: 0px;
margin: 0.25rem 2px;
border-radius: 0.2rem;
}
.aq-item {
background-color: #464646;
padding: 12px;
margin: 12px;
cursor: move;
}
.aq-item-inner {
display: flex;
justify-content: space-between;
cursor: move;
}
.aq-delete {
font-size: 1.2rem;
padding: 0px 0.36rem;
line-height: 0px;
margin: 0.25rem 0.25rem 0.25rem 0.125rem;
border-radius: 0.2rem;
}
.aq-grey {
background-color: #676767;
}
.aq-grey:hover {
background-color: #848484;
}
.aq-blue {
background-color: #0083ff;
}
.aq-blue:hover {
background-color: #63b4ff;
}
.aq-green {
background-color: #5a9e00;
}
.aq-green:hover {
background-color: #7bd900;
}
.aq-yellow {
background-color: #e69721;
}
.aq-yellow:hover {
background-color: #ffb445;
}
.t-drag {
opacity: 0.5;
}
.a-drag {
opacity: 0.5;
}
.aq-popup {
position: fixed;
right: 5%;
top: 20%;
z-index: 2;
background-color: #3f4046;
padding: 20px 20px 20px 0;
border-top: 1px solid #e1e6e9;
border-radius: 0.25rem;
border-width: 4px;
}
</style>
`;
function loadAQ() {
//add item names
for (const a of items) {
options.triggers["Item Quantity"][a.name] = { "≥": "num", "≤": "num" };
options.actions["Sell Item"][a.name] = null;
}
//add pet names
for (const pet of PETS) {
const name = `${pet.name.split(",")[0]} (${pet.acquiredBy})`;
options.triggers["Pet Unlocked"][name] = null;
}
//add skill names
Object.keys(CONSTANTS.skill).forEach((a) => {
options.triggers["Skill Level"][a] = "num";
});
options.triggers["Skill XP"] = options.triggers["Skill Level"];
Object.keys(options.triggers["Mastery Level"]).forEach((skill) => (options.triggers["Mastery Pool %"][skill] = "num"));
//add mastery/action names for each skill
{
cookingItems.forEach((item) => {
options.triggers["Mastery Level"]["Cooking"][items[item.itemID].name] = "num";
options.actions["Start Skill"]["Cooking"][items[item.itemID].name] = null;
});
craftingItems.forEach((item) => {
options.triggers["Mastery Level"]["Crafting"][items[item.itemID].name] = "num";
options.actions["Start Skill"]["Crafting"][items[item.itemID].name] = null;
});
items.forEach((item) => {
if (item.type == "Seeds") {
options.triggers["Mastery Level"]["Farming"][item.name] = "num";
}
});
items.forEach((item) => {
if (item.type == "Logs") {
options.triggers["Mastery Level"]["Firemaking"][item.name] = "num";
options.actions["Start Skill"]["Firemaking"][item.name] = null;
}
});
fishingItems.forEach((item) => {
options.triggers["Mastery Level"]["Fishing"][items[item.itemID].name] = "num";
options.actions["Start Skill"]["Fishing"][items[item.itemID].name] = null;
});
fletchingItems.forEach((item) => {
options.triggers["Mastery Level"]["Fletching"][items[item.itemID].name] = "num";
options.actions["Start Skill"]["Fletching"][items[item.itemID].name] = null;
});
herbloreItemData.forEach((item) => {
options.triggers["Mastery Level"]["Herblore"][item.name] = "num";
options.actions["Start Skill"]["Herblore"][item.name] = null;
});
miningData.forEach((item) => {
options.triggers["Mastery Level"]["Mining"][items[item.ore].name] = "num";
options.actions["Start Skill"]["Mining"][items[item.ore].name] = null;
});
runecraftingItems.forEach((item) => {
options.triggers["Mastery Level"]["Runecrafting"][items[item.itemID].name] = "num";
options.actions["Start Skill"]["Runecrafting"][items[item.itemID].name] = null;
});
smithingItems.forEach((item) => {
options.triggers["Mastery Level"]["Smithing"][items[item.itemID].name] = "num";
options.actions["Start Skill"]["Smithing"][items[item.itemID].name] = null;
});
thievingNPC.forEach((npc) => {
options.triggers["Mastery Level"]["Thieving"][npc.name] = "num";
options.actions["Start Skill"]["Thieving"][npc.name] = null;
});
items.forEach((item) => {
if (item.type == "Logs") {
options.triggers["Mastery Level"]["Woodcutting"][item.name] = "num";
options.actions["Start Skill"]["Woodcutting"][item.name] = {};
}
});
for (const log in options.actions["Start Skill"]["Woodcutting"]) {
Object.keys(options.actions["Start Skill"]["Woodcutting"]).forEach(
(a) => (options.actions["Start Skill"]["Woodcutting"][log][a] = null)
);
}
}
//add altmagic names
ALTMAGIC.forEach((spell) => {
options.actions["Start Skill"]["Magic"][spell.name] = {};
if (spell.selectItem === 0) {
for (const item of smithingItems) {
if (item.category === 0) {
options.actions["Start Skill"]["Magic"][spell.name][items[item.itemID].name] = null;
}
}
} else if (spell.isJunk) {
junkItems.forEach((a) => (options.actions["Start Skill"]["Magic"][spell.name][items[a].name] = null));
} else if (spell.selectItem === 1) {
options.actions["Start Skill"]["Magic"][spell.name] = options.actions["Sell Item"];
} else options.actions["Start Skill"]["Magic"][spell.name] = null;
});
//add food and ammo names
for (const a of items.filter((a) => a.canEat)) {
options.triggers["Equipped Item Quantity"][a.name] = "num";
options.actions["Equip Item"][a.name] = null;
}
for (const a of items.filter((a) => a.equipmentSlot == CONSTANTS.equipmentSlot.Quiver))
options.triggers["Equipped Item Quantity"][a.name] = "num";
//add monster/dungeon names
{
//collect monster IDs
const monsterIDs = [];
for (const area of combatAreas) monsterIDs.push(...area.monsters);
for (const area of slayerAreas) monsterIDs.push(...area.monsters);
//add names to array
for (const monster of monsterIDs) options.actions["Start Combat"][MONSTERS[monster].name] = null;
for (const dungeon of DUNGEONS) options.actions["Start Combat"][dungeon.name] = null;
}
//add equippable items
for (const a of items.filter((a) => a.hasOwnProperty("equipmentSlot"))) {
options.actions["Equip Item"][a.name] = null;
options.actions["Unequip Item"][a.name] = null;
}
//fill shop object with ShopItems
//bank upgrade
shop["Upgrade Bank"] = new ShopItem(
0,
0,
() => {
if (currentGamemode === 1 && baseBankMax + bankMax >= 80) return false;
return gp >= Math.min(newNewBankUpgradeCost.level_to_gp(currentBankUpgrade + 1), 5000000);
},
() => upgradeBank(true)
);
//autoeat upgrade
shop["Upgrade Auto Eat"] = new ShopItem(
0,
0,
() => currentAutoEat < 3 && gp >= autoEatData[currentAutoEat].cost,
() => upgradeAutoEat(true)
);
//equipsets
shop["Equipment Set (gp)"] = new ShopItem(
equipmentSetData[0].cost,
0,
() => !equipmentSetsPurchased[0],
() => upgradeEquipmentSet(0, true)
);
shop["Equipment Set (slayer coins)"] = new ShopItem(
0,
equipmentSetData[1].cost,
() => !equipmentSetsPurchased[1],
() => upgradeEquipmentSet(1, true)
);
//dungeon swap
shop["Dungeon Equipment Swapping"] = new ShopItem(
equipmentSwapData[0].cost,
0,
() => !equipmentSetsPurchased[1],
() => upgradeEquipmentSwap(true)
);
//multitree
shop["Multi-Tree"] = new ShopItem(
multiTreeCost[0],
0,
() => treeCutLimit < 2,
() => upgradeMultiTree(true)
);
//godupgrades
godUpgradeData.forEach((upgrade, i) => {
shop[upgrade.name] = new ShopItem(
upgrade.cost,
0,
() => dungeonCompleteCount[upgrade.dungeonID] > 0 && !godUpgrade[i],
() => buyGodUpgrade(i, true)
);
});
//skill upgrades
shop["Upgrade Axe"] = new ShopItem(
0,
0,
() => currentAxe < 7 && gp >= axeCost[currentAxe + 1],
() => upgradeAxe(true)
);
shop["Upgrade Rod"] = new ShopItem(
0,
0,
() => currentRod < 7 && gp >= rodCost[currentRod + 1],
() => upgradeRod(true)
);
shop["Upgrade Pickaxe"] = new ShopItem(
0,
0,
() => currentPickaxe < 7 && gp >= pickaxeCost[currentPickaxe + 1],
() => upgradePickaxe(true)
);
shop["Upgrade Cooking Fire"] = new ShopItem(
0,
0,
() => {
return (
currentCookingFire < 9 &&
gp >= cookingFireData[currentCookingFire].costGP &&
skillLevel[CONSTANTS.skill.Firemaking] >= cookingFireData[currentCookingFire].fmLevel &&
getBankQty(cookingFireData[currentCookingFire].costLogs[0]) >= cookingFireData[currentCookingFire].costLogs[1]
);
},
() => upgradeCookingFire(true)
);
//slayer equipment
for (const itemID of slayerItems) {
shop[items[itemID].name] = new ShopItem(
0,
items[itemID].slayerCost,
() => true,
() => buySlayerItem(itemID, true)
);
}
//gloves
gloveID.forEach((itemID, i) => {
shop[items[itemID].name] = new ShopItem(
glovesCost[i],
0,
() => true,
() => buyGloves(i, true)
);
});
//skillcapes
skillcapeItems.forEach((itemID, i) => {
shop[items[itemID].name] = new ShopItem(
items[itemID].buysFor,
0,
() => true,
() => buySkillcape(i, true)
);
});
for (const name in shop) options.actions["Buy Item"][name] = null;
//materials
shop[items[CONSTANTS.item.Feathers].name] = new ShopItem(
0,
0,
(qty) => gp >= items[CONSTANTS.item.Feathers].buysFor * qty,
(qty) => {
updateBuyQty(qty);
buyFeathers(true);
}
);
shop[items[CONSTANTS.item.Compost].name] = new ShopItem(
0,
0,
(qty) => gp >= items[CONSTANTS.item.Compost].buysFor * qty,
(qty) => {
updateBuyQty(qty);
buyCompost(true);
}
);
shop[items[CONSTANTS.item.Weird_Gloop].name] = new ShopItem(
0,
0,
(qty) => {
for (const a of items[CONSTANTS.item.Weird_Gloop].buysForItems) {
if (getBankQty(a[0]) < qty * a[1]) return false;
}
return gp >= items[CONSTANTS.item.Weird_Gloop].buysFor * qty;
},
(qty) => {
updateBuyQty(qty);
buyItem(CONSTANTS.item.Weird_Gloop, true);
}
);
shop[items[CONSTANTS.item.Bowstring].name] = new ShopItem(
0,
0,
(qty) => gp >= items[CONSTANTS.item.Bowstring].buysFor * qty,
(qty) => {
updateBuyQty(qty);
buyBowstring(true);
}
);
shop[items[CONSTANTS.item.Leather].name] = new ShopItem(
0,
0,
(qty) => gp >= items[CONSTANTS.item.Leather].buysFor * qty,
(qty) => {
updateBuyQty(qty);
buyLeather(true);
}
);
[CONSTANTS.item.Green_Dragonhide, CONSTANTS.item.Blue_Dragonhide, CONSTANTS.item.Red_Dragonhide].forEach((itemID) => {
shop[items[itemID].name] = new ShopItem(
0,
0,
(qty) => gp >= items[itemID].buysFor * qty,
(qty) => {
updateBuyQty(qty);
buyDhide(itemID, true);
}
);
});
shop[items[CONSTANTS.item.Red_Party_Hat].name] = new ShopItem(
0,
0,
(qty) => gp >= items[CONSTANTS.item.Red_Party_Hat].buysFor * qty,
(qty) => {
updateBuyQty(qty);
buyPartyHat(true);
}
);
//add array of shop items
for (const name in shop) if (!Object.keys(options.actions["Buy Item"]).includes(name)) options.actions["Buy Item"][name] = "num";
//add in sidebar item
$("li.nav-main-item:contains(Bank)")[0].insertAdjacentHTML(
"afterend",
`<li class="nav-main-item">
<a class="nav-main-link nav-compact" style="cursor: pointer;">
<img class="nav-img" src="assets/media/skills/prayer/mystic_lore.svg">
<span class="nav-main-link-name">Action Queue</span>
<small id="current-queue" style="color: rgb(210, 106, 92);">inactive</small>
</a>
</li>`
);
//add click for sidebar item
$("li.nav-main-item:contains(Action Queue)")[0].addEventListener("click", () => actionTab());
const htmlCollection = $('div[onclick^="changePage"]');
for (let i = 0; i < htmlCollection.length; i++) htmlCollection[i].addEventListener("click", () => hideActionTab());
//add main html
document.getElementById("main-container").insertAdjacentHTML("beforeend", aqHTML);
//add button clicks
document.getElementById("aq-pause").addEventListener("click", () => togglePause());
document.getElementById("aq-download").addEventListener("click", () => downloadActions());
document.getElementById("aq-loop-enable").addEventListener("click", () => toggleLoop(true));
document.getElementById("aq-loop-disable").addEventListener("click", () => toggleLoop(false));
document.getElementById("aq-mastery-enable").addEventListener("click", () => toggleMastery(true));
document.getElementById("aq-mastery-disable").addEventListener("click", () => toggleMastery(false));
document.getElementById("aq-cancel").addEventListener("click", () => cancelEdit());
document.getElementById("aq-save-edit").addEventListener("click", () => submitEdit());
document.getElementById("aq-form").addEventListener("submit", (e) => {
e.preventDefault();
submitForm();
});
document.getElementById("aq-item-container").parentNode.children[1].addEventListener("submit", (e) => {
e.preventDefault();
importActions();
});
for (const menu in validInputs) {
validInputs[menu].forEach((a, i) => {
document.getElementById(`aq-text${menu}${i}`).addEventListener("input", () => {
dropdowns(menu);
});
});
}
//fills category lists
Object.keys(options.triggers).forEach((a) =>
document.getElementById("aq-listA0").insertAdjacentHTML("beforeend", `<option>${a}</option>`)
);
Object.keys(options.actions).forEach((a) =>
document.getElementById("aq-listB0").insertAdjacentHTML("beforeend", `<option>${a}</option>`)
);
//add event listener for dragging trigger blocks
const triggerContainer = document.getElementById("aq-item-container");
triggerContainer.addEventListener("dragover", (e) => {
e.preventDefault();
const draggable = document.querySelector(".t-drag");
if (!draggable || document.querySelector(".a-drag") != null) return;
const afterElement = getDragAfterElement(triggerContainer, e.clientY, ".aq-item");
if (afterElement == null) {
triggerContainer.appendChild(draggable);
} else {
triggerContainer.insertBefore(draggable, afterElement);
}
});
//load locally stored action queue if it exists
loadLocalSave();
console.log("Action Queue loaded");
}
const replaceChar = [
/*{ reg: "&", replace: "&" },
{ reg: '"', replace: """ },
{ reg: "£", replace: "£" },
{ reg: "€", replace: "€" },
{ reg: "é", replace: "é" },
{ reg: "–", replace: "–" },
{ reg: "®", replace: "®" },
{ reg: "™", replace: "™" },
{ reg: "‘", replace: "‘" },
{ reg: "’", replace: "’" },
{ reg: "“", replace: "“" },
{ reg: "”", replace: "”" },
{ reg: "#", replace: "#" },
{ reg: "©", replace: "©" },
{ reg: "@", replace: "@" },
{ reg: "$", replace: "$" },
{ reg: "\\(", replace: "(" },
{ reg: "\\)", replace: ")" },
{ reg: "<", replace: "<" },
{ reg: ">", replace: ">" },
{ reg: "…", replace: "…" },
{ reg: "-", replace: "-" },*/
{ reg: "'", replace: "'" },
/* { reg: "\\*", replace: "*" },
{ reg: ",", replace: "‚" },*/
];
//replaces special characters with their html equivalent
function htmlChar(string) {
let s = string;
replaceChar.forEach(function (obj) {
const regEx = new RegExp(obj.reg + "(?!([^<]+)?>)", "g");
s = s.replace(regEx, obj.replace);
});
return s;
}
function triggerCheck() {
let result = true;
if (currentActionIndex >= actionQueueArray.length) {
if (queueLoop && actionQueueArray.length > 0) {
currentActionIndex = 0;
updateQueue();
return;
} else {
clearInterval(triggerCheckInterval);
triggerCheckInterval = null;
updateTextColour("stop");
return;
}
}
if (actionQueueArray[currentActionIndex].trigger()) {
actionQueueArray[currentActionIndex].action.forEach((action, i) => {
result = action.start();
document.getElementById(actionQueueArray[currentActionIndex].elementID).children[1].children[
i
].children[1].children[0].style.display = result ? "none" : "";
});
currentActionIndex + 1 >= actionQueueArray.length && queueLoop ? (currentActionIndex = 0) : currentActionIndex++;
updateQueue();
}
}
/**
* Updates colour and text in sidebar
* @param {string} type ("start" || "stop" || "pause")
*/
function updateTextColour(type) {
switch (type) {
case "start":
document.getElementById("current-queue").style.color = "#46c37b";
document.getElementById("current-queue").innerHTML = "running";
break;
case "stop":
document.getElementById("current-queue").style.color = "#d26a5c";
document.getElementById("current-queue").innerHTML = "inactive";
break;
case "pause":
document.getElementById("current-queue").style.color = "#f3b760";
document.getElementById("current-queue").innerHTML = "paused";
}
}
function updateQueue() {
actionQueueArray.forEach((action, index) => {
const element = document.getElementById(action.elementID);
if (index === currentActionIndex) {
for (let i = 0; i < element.children[1].children.length; i++) {
element.children[1].children[i].children[1].children[0].style.display = "none";
}
element.style.backgroundColor = "#385a0b";
} else element.style.backgroundColor = "";
});
}
function toggleLoop(start) {
queueLoop = start;
}
function togglePause() {
queuePause = !queuePause;
if (queuePause) {
clearInterval(triggerCheckInterval);
triggerCheckInterval = null;
document.getElementById("aq-pause").innerHTML = "Unpause";
document.getElementById("aq-pause").classList.add("aq-green");
document.getElementById("aq-pause").classList.remove("aq-yellow");
updateTextColour("pause");
} else {
if (actionQueueArray.length > 0) {
updateQueue();
triggerCheckInterval = setInterval(() => {
triggerCheck();
}, 1000);
updateTextColour("start");
} else {
updateTextColour("stop");
}
document.getElementById("aq-pause").innerHTML = "Pause";
document.getElementById("aq-pause").classList.add("aq-yellow");
document.getElementById("aq-pause").classList.remove("aq-green");
}
}
let loadCheckInterval = setInterval(() => {
if (isLoaded) {
clearInterval(loadCheckInterval);
loadAQ();
}
}, 200);
function autoSave() {
const saveData = { index: currentActionIndex, data: [] };
for (const action of actionQueueArray) {
let actionList = [];
action.action.forEach((a) => actionList.push(a.data));
saveData.data.push([...action.data, actionList]);
}
window.localStorage.setItem("AQSAVE" + currentCharacter, JSON.stringify(saveData));
}
//autosave every ~minute
setInterval(() => {
autoSave();
}, 59550);
function loadLocalSave() {
const obj = JSON.parse(window.localStorage.getItem("AQSAVE" + currentCharacter));
if (obj === null) return;
if (obj.data.length > 0) togglePause();
currentActionIndex = obj.index;
for (const params of obj.data) {
if (params.length == 6) {
const newAction = new Action(...params.slice(0, 5));
params[5].forEach((data) => {
newAction.action.push({
elementID: `AQ${nameIncrement++}`,
data,
start: setAction(...data),
description: actionDescription(...data),
});
});
addToQueue(newAction);
} else {
addToQueue(new Action(...params));
}
}
updateQueue();
}
function setCurrentAction(id) {
const index = actionQueueArray.findIndex((a) => a.elementID == id);
if (index >= 0) {
currentActionIndex = index;
updateQueue();
}
}
function importActions() {
const string = document.getElementById("aq-pastebin").value;
if (!queuePause && actionQueueArray.length === 0) togglePause();
let arr = [];
try {
arr = JSON.parse(string.trim());
if (!Array.isArray(arr)) return false;
for (const params of arr) {
try {
let newAction = null;
if (params.length == 10) {
newAction = new Action(...params);
} else if (params.length == 6) {
newAction = new Action(...params.slice(0, 5));
params[5].forEach((data) => {
newAction.action.push({
elementID: `AQ${nameIncrement++}`,
data,
start: setAction(...data),
description: actionDescription(...data),
});
});
}
if (
!newAction.description.includes("undefined") &&
!newAction.description.includes("null") &&
typeof newAction.trigger == "function" &&
newAction.action.every((a) => {
return !a.description.includes("undefined") && !a.description.includes("null") && typeof a.start == "function";
})
)
addToQueue(newAction);
} catch {}
}
document.getElementById("aq-pastebin").value = "";
updateQueue();
} catch (e) {
console.error(e);
} finally {
return false;
}
}
function downloadActions() {
const saveData = [];
for (const action of actionQueueArray) {
let actionList = [];
action.action.forEach((a) => actionList.push(a.data));
saveData.push([...action.data, actionList]);
}
let file = new Blob([JSON.stringify(saveData)], {
type: "text/plain",
});
if (window.navigator.msSaveOrOpenBlob) window.navigator.msSaveOrOpenBlob(file, "Melvor_Action_Queue.txt");
else {
var a = document.createElement("a"),
url = URL.createObjectURL(file);
a.href = url;
a.download = "Melvor_Action_Queue.txt";
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
}
function manageMastery() {
for (const skill in MASTERY) {
let lowest = 0;
for (var i = 1; i < MASTERY[skill].xp.length; i++) {
if (MASTERY[skill].xp[i] < MASTERY[skill].xp[lowest]) lowest = i;
}
const masteryID = lowest;
const maxPool = MASTERY[skill].xp.length * 500000;
const bankID = getBankId(CONSTANTS.item[`Mastery_Token_${SKILLS[skill].name}`]);
if (bankID !== false && bank[bankID].qty > 0 && MASTERY[skill].pool < maxPool * 0.999) {
const maxTokens = Math.floor(((maxPool - MASTERY[skill].pool) * 1000) / maxPool);
{
let itemID = CONSTANTS.item[`Mastery_Token_${SKILLS[skill].name}`];
let qtyToUse = Math.min(bank[bankID].qty, maxTokens);
let totalXpToAdd = Math.floor(getMasteryPoolTotalXP(skill) * 0.001) * qtyToUse;
addMasteryXPToPool(skill, totalXpToAdd, false, true);
notifyPlayer(skill, numberWithCommas(totalXpToAdd) + " Mastery Pool XP granted.", "success");
updateItemInBank(bankID, itemID, -qtyToUse);
}
}
if (
MASTERY[skill].xp[masteryID] < 13034432 &&
(MASTERY[skill].pool == maxPool || MASTERY[skill].pool - getMasteryXpForNextLevel(skill, masteryID) > 0.95 * maxPool)
) {
if (masteryPoolLevelUp > 1) masteryPoolLevelUp = 1;
let xp = getMasteryXpForNextLevel(skill, masteryID);
if (MASTERY[skill].pool >= xp) {
{
let currentLevel = getMasteryLevel(skill, masteryID);
MASTERY[skill].xp[masteryID] += xp;
if (exp.xp_to_level(MASTERY[skill].xp[masteryID]) - 1 > currentLevel && currentLevel < 99) {
if (skill === CONSTANTS.skill.Woodcutting && currentLevel >= 98) updateWCRates();
}
updateMasteryProgress(skill, masteryID);
}
MASTERY[skill].pool -= xp;
updateMasteryPoolProgress(skill);
}
if (skill === CONSTANTS.skill.Fishing) {
for (let i = 0; i < fishingAreas.length; i++) {
for (let f = 0; f < fishingAreas[i].fish.length; f++) {
if (fishingAreas[i].fish[f] === masteryID) {
updateFishingMastery(i, f);
break;
}
}
}
}
}
}
}
function toggleMastery(start) {
if (start && manageMasteryInterval === null) {
manageMasteryInterval = setInterval(() => {
manageMastery();
}, 1000);
} else if (!start) {
clearInterval(manageMasteryInterval);
manageMasteryInterval = null;
}
}
function addToQueue(obj) {
actionQueueArray.push(obj);
document.getElementById("aq-item-container").insertAdjacentHTML(
"beforeend",
`<div class="aq-item" id="${obj.elementID}" draggable="true">
<div class="aq-item-inner">
<p style="margin: auto 0">${obj.description}</p>
<div style="min-width: 170px; min-height: 39px; display: flex; justify-content: flex-end;">
<button type="button" class="btn aq-arrow aq-grey" style="font-size: 0.875rem;">select</button>
<button type="button" class="btn aq-arrow aq-grey" style="padding: 0 0.1rem 0.2rem 0.1rem;">+</button>
<button type="button" class="btn aq-arrow aq-grey" style="padding:0 0.09rem;">
<svg width="22" height="22" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.2929 9.8299L19.9409 9.18278C21.353 7.77064 21.353 5.47197 19.9409 4.05892C18.5287 2.64678 16.2292 2.64678 14.817 4.05892L14.1699 4.70694L19.2929 9.8299ZM12.8962 5.97688L5.18469 13.6906L10.3085 18.813L18.0201 11.0992L12.8962 5.97688ZM4.11851 20.9704L8.75906 19.8112L4.18692 15.239L3.02678 19.8796C2.95028 20.1856 3.04028 20.5105 3.26349 20.7337C3.48669 20.9569 3.8116 21.046 4.11851 20.9704Z" fill="currentColor"></path>
</svg>
</button>
<button type="button" class="btn aq-delete btn-danger">X</button>
</div>
</div>
<div style="min-height: 39px; padding-left: 10px;"></div>
</div>`
);
//add button clicks
const buttons = document.getElementById(obj.elementID).children[0].children[1].children;
buttons[0].addEventListener("click", () => setCurrentAction(obj.elementID));
buttons[1].addEventListener("click", () => editQueue(obj.elementID, "add"));
buttons[2].addEventListener("click", () => editQueue(obj.elementID, "triggers"));
buttons[3].addEventListener("click", () => deleteAction(obj.elementID, "trigger"));
//add tooltips
tooltips[obj.elementID] = [
tippy(document.getElementById(obj.elementID).children[0].children[1].children[0], {
content: "Set as current trigger",
animation: false,
}),
tippy(document.getElementById(obj.elementID).children[0].children[1].children[1], { content: "Add action", animation: false }),
tippy(document.getElementById(obj.elementID).children[0].children[1].children[2], { content: "Edit trigger", animation: false }),
tippy(document.getElementById(obj.elementID).children[0].children[1].children[3], {
content: "Delete trigger & actions",
animation: false,
}),
];
//add eventlisteners for dragging trigger block
const element = document.getElementById(obj.elementID);
element.addEventListener("dragstart", () => {
element.classList.add("t-drag");
});
element.addEventListener("dragend", () => {
reorderQueue();
element.classList.remove("t-drag");
});
//append html for each action
obj.action.forEach((action) => {
document.getElementById(obj.elementID).children[1].insertAdjacentHTML(
"beforeend",
`<div id='${action.elementID}' class="aq-item-inner" draggable="true">
<p style="margin: auto 0">${action.description}</p>
<div style="min-width: 170px; min-height: 39px; display: flex; justify-content: flex-end;">
<small style="display: none; margin: auto 0.25rem">action failed</small>
<button type="button" class="btn aq-arrow aq-grey" style="padding:0 0.09rem;">
<svg width="22" height="22" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.2929 9.8299L19.9409 9.18278C21.353 7.77064 21.353 5.47197 19.9409 4.05892C18.5287 2.64678 16.2292 2.64678 14.817 4.05892L14.1699 4.70694L19.2929 9.8299ZM12.8962 5.97688L5.18469 13.6906L10.3085 18.813L18.0201 11.0992L12.8962 5.97688ZM4.11851 20.9704L8.75906 19.8112L4.18692 15.239L3.02678 19.8796C2.95028 20.1856 3.04028 20.5105 3.26349 20.7337C3.48669 20.9569 3.8116 21.046 4.11851 20.9704Z" fill="currentColor"></path>
</svg>
</button>
<button type="button" class="btn aq-delete btn-danger">X</button>
</div>
</div>`
);
//add tooltips
tooltips[action.elementID] = [
tippy(document.getElementById(action.elementID).children[1].children[1], { content: "Edit Action", animation: false }),
tippy(document.getElementById(action.elementID).children[1].children[2], { content: "Delete Action", animation: false }),
];
//add eventlisteners for dragging actions
const element = document.getElementById(action.elementID);
element.addEventListener("dragstart", () => {
element.classList.add("a-drag");
});
element.addEventListener("dragend", () => {
element.classList.remove("a-drag");
});
//add button clicks
const buttons = element.children[1].children;
buttons[1].addEventListener("click", () => editQueue(action.elementID, "actions"));
buttons[2].addEventListener("click", () => deleteAction(action.elementID, "action"));
});
//add eventlistener for dragging actions within trigger blocks
const container = document.getElementById(obj.elementID).children[1];
container.addEventListener("dragover", (e) => {
e.preventDefault();
const draggable = document.querySelector(".a-drag");
if (!draggable) return;
const afterElement = getDragAfterElement(container, e.clientY, ".aq-item-inner");
if (afterElement == null) {
container.appendChild(draggable);
} else {
container.insertBefore(draggable, afterElement);
}
});
if (triggerCheckInterval === null && !queuePause) {
currentActionIndex = 0;
updateQueue();
triggerCheckInterval = setInterval(() => {
triggerCheck();
}, 1000);
updateTextColour("start");
}
}
function deleteAction(id, type) {
tooltips[id].forEach((a) => a.destroy());
delete tooltips[id];
if (type == "trigger") {
const i = actionQueueArray.findIndex((a) => a.elementID == id);
if (i < 0) return;
for (const action of actionQueueArray[i].action) {
tooltips[action.elementID].forEach((a) => a.destroy());
delete tooltips[action.elementID];
}
//remove from array
actionQueueArray.splice(i, 1);
if (currentActionIndex > i) currentActionIndex--;
updateQueue();
} else if (type == "action") {
let i = 0;
for (const trigger of actionQueueArray) {
i = trigger.action.findIndex((a) => a.elementID == id);
if (i < 0) continue;
trigger.action.splice(i, 1);
break;
}
}
//remove html
const element = document.getElementById(id);
if (element) element.remove();
}
function addAction(id, actionCategory, actionName, skillItem, skillItem2, qty) {
let elementID = `AQ${nameIncrement++}`;
let description = actionDescription(actionCategory, actionName, skillItem, skillItem2, qty);
actionQueueArray
.find((a) => a.elementID == id)
.action.push({
elementID,
data: [actionCategory, actionName, skillItem, skillItem2, qty],
start: setAction(actionCategory, actionName, skillItem, skillItem2, qty),
description,
});
document.getElementById(id).children[1].insertAdjacentHTML(
"beforeend",
`<div id='${elementID}' class="aq-item-inner" draggable="true">
<p style="margin: auto 0">${description}</p>
<div style="min-width: 170px; min-height: 39px; display: flex; justify-content: flex-end;">
<small style="display: none; margin: auto 0.25rem">action failed</small>
<button type="button" class="btn aq-arrow aq-grey" style="padding:0 0.09rem;">
<svg width="22" height="22" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.2929 9.8299L19.9409 9.18278C21.353 7.77064 21.353 5.47197 19.9409 4.05892C18.5287 2.64678 16.2292 2.64678 14.817 4.05892L14.1699 4.70694L19.2929 9.8299ZM12.8962 5.97688L5.18469 13.6906L10.3085 18.813L18.0201 11.0992L12.8962 5.97688ZM4.11851 20.9704L8.75906 19.8112L4.18692 15.239L3.02678 19.8796C2.95028 20.1856 3.04028 20.5105 3.26349 20.7337C3.48669 20.9569 3.8116 21.046 4.11851 20.9704Z" fill="currentColor"></path>
</svg>
</button>
<button type="button" class="btn aq-delete btn-danger">X</button>
</div>
</div>`
);
const element = document.getElementById(elementID);
//add tooltips
tooltips[elementID] = [
tippy(element.children[1].children[1], { content: "Edit Action", animation: false }),
tippy(element.children[1].children[2], { content: "Delete Action", animation: false }),
];
//add eventlisteners for dragging
element.addEventListener("dragstart", () => {
element.classList.add("a-drag");
});
element.addEventListener("dragend", () => {
element.classList.remove("a-drag");
});
//add button clicks
const buttons = element.children[1].children;
buttons[1].addEventListener("click", () => editQueue(elementID, "actions"));
buttons[2].addEventListener("click", () => deleteAction(elementID, "action"));
}
function getDragAfterElement(container, y, type) {
const not = type == "aq-item" ? ".t-drag" : ".a-drag";
const elements = [...container.querySelectorAll(`${type}:not(${not})`)];
return elements.reduce(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
},
{ offset: Number.NEGATIVE_INFINITY }
).element;
}
function reorderQueue() {
const targetIndex = actionQueueArray[currentActionIndex].elementID;
//remove stray dragging classes
document
.getElementById("aq-item-container")
.querySelectorAll(".a-drag")
.forEach((a) => a.classList.remove("a-drag"));
document
.getElementById("aq-item-container")
.querySelectorAll(".t-drag")
.forEach((a) => a.classList.remove("t-drag"));
//sort triggers
const triggerOrder = {};
[...document.getElementById("aq-item-container").children].forEach((item, index) => (triggerOrder[item.id] = index));
actionQueueArray.sort((a, b) => triggerOrder[a.elementID] - triggerOrder[b.elementID]);
//sort actions
const actionList = actionQueueArray.reduce((a, b) => a.concat(b.action), []);
let increment = -1;
[...document.getElementById("aq-item-container").querySelectorAll(".aq-item-inner")].forEach((item) => {
if (item.id == "") {
increment++;
actionQueueArray[increment].action = [];
} else {
actionQueueArray[increment].action.push(actionList.find((a) => a.elementID == item.id));
}
});
//reorder currentActionIndex
currentActionIndex = actionQueueArray.findIndex((a) => a.elementID == targetIndex);
}
function editQueue(id, type) {
cancelEdit(); //reset form
currentlyEditing = { id, type };
let inputArray = [];
let obj = options[type];
//set inputArray
if (type == "triggers") {
inputArray = actionQueueArray.find((a) => a.elementID == id).data.filter((a) => a !== null && a !== "");
document.getElementById("aq-edit-form").innerHTML = "Edit Trigger";
} else if (type == "actions") {
document.getElementById("aq-edit-form").innerHTML = "Edit Action";
for (const item of actionQueueArray) {
if (item.action.find((a) => a.elementID == id)) {
inputArray = item.action.find((a) => a.elementID == id).data.filter((a) => a !== null && a !== "");
break;
}
}
} else {
document.getElementById("aq-edit-form").innerHTML = "New Action";
obj = options.actions;
}
//set datalist for category
document.getElementById("aq-listC0").innerHTML = "";
Object.keys(obj).forEach((e) => {
document.getElementById("aq-listC0").insertAdjacentHTML("beforeend", `<option>${e}</option>`);
});
//populate text boxes to edit
for (let i = 0; i < inputArray.length; i++) {
let textBox;
if (/^\d{1,10}$/.test(inputArray[i])) {
textBox = document.getElementById("aq-numC");
} else {
textBox = document.getElementById(`aq-textC${i}`);
//set datalist
document.getElementById(`aq-listC${i}`).innerHTML = "";
Object.keys(obj).forEach((e) => {
document.getElementById(`aq-listC${i}`).insertAdjacentHTML("beforeend", `<option>${e}</option>`);
});
obj = obj[inputArray[i]];
}
textBox.value = inputArray[i];
textBox.type = "text";
validInputs.C[i] = inputArray[i];
}
const y = Math.max(document.getElementById(id).getBoundingClientRect().top - 255, 0);
document.getElementById("aq-edit-container").style.top = `${y}px`;
document.getElementById("aq-edit-container").style.display = "";
}
function cancelEdit() {
document.getElementById("aq-edit-container").style.display = "none";
resetForm(["C"]);
}
function submitEdit() {
const a = document.getElementById("aq-edit-form").innerHTML.includes("Trigger");
const arr = [];
for (let i = 0; i < validInputs.C.length; i++) arr.push(htmlChar(document.getElementById(`aq-textC${i}`).value));
if (a) arr.pop();
arr.push(document.getElementById(`aq-numC`).value);
if (a) arr.splice(["≥", "≤", ""].includes(arr[2]) ? 3 : 2, 0, "");
if (
validateInput(
a ? options.triggers : options.actions,
arr.filter((a) => a !== "")
)
) {
if (currentlyEditing.type == "add") {
addAction(currentlyEditing.id, ...arr);
} else if (currentlyEditing.type == "triggers") {
const action = new Action(...arr);
const i = actionQueueArray.findIndex((a) => a.elementID == currentlyEditing.id);
actionQueueArray[i].description = action.description;
actionQueueArray[i].trigger = action.trigger;
actionQueueArray[i].data = action.data;
document.getElementById(currentlyEditing.id).children[0].children[0].innerHTML = action.description;
} else {
const description = actionDescription(...arr);
const start = setAction(...arr);
for (const item of actionQueueArray) {
const action = item.action.find((a) => a.elementID == currentlyEditing.id);
if (action) {
action.data = arr;
action.start = start;
action.description = description;
document.getElementById(currentlyEditing.id).children[0].innerHTML = description;
break;
}
}
}
cancelEdit();
}
return false;
}