您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A script that lets you know more info about quests and other QOL improvements
// ==UserScript== // @name Queslar Betterment Script // @namespace https://www.queslar.com // @version 1.8.0 // @description A script that lets you know more info about quests and other QOL improvements // @author RiddleMeDoo // @match *://*.queslar.com/* // @require https://code.jquery.com/jquery-3.6.3.slim.min.js // @resource settingsMenu https://raw.githubusercontent.com/RiddleMeDoo/qs-bettermentScript/master/tomeSettingsMenu.html // @grant GM_getResourceText // ==/UserScript== class Script { constructor() { // Get quest data this.quest = { questsCompleted: 0, numRefreshes: 0, refreshesUsed: 0, villageBold: 0, villageSize: 1, villageNumRefreshes: 5, villageRefreshesUsed: 0, baseStat: 15, minActions: 360, maxActions: 580, }; this.catacomb = { actionTimerSeconds: 30, tomesAreEquipped: true, } this.kdExploLevel = 0; this.playerId; this.gameData; this.gems = []; //observer setup this.initObservers(); this.currentPath = window.location.hash.split('/').splice(2).join(); // Other setup addInvisibleScrollDiv(); } loadDataFromStorage() { /** * Load data stored in the localStorage of the website. Each player stores their own settings. */ // ! BANDAID migration, please remove non-id settings in storage after 2024-12-01 this.villageSettings = JSON.parse(localStorage.getItem(`${this.playerId}:QuesBS_villageSettings`)); if (!this.villageSettings && localStorage.getItem('QuesBS_villageSettings')) { // Attempt migration from old settings this.villageSettings = JSON.parse(localStorage.getItem('QuesBS_villageSettings')); localStorage.setItem(`${this.playerId}:QuesBS_villageSettings`, JSON.stringify(this.villageSettings)); localStorage.removeItem('QuesBS_villageSettings'); } else if(!this.villageSettings) { this.villageSettings = { strActions: 30000, taxObjectivePercent: 0, resActionRatio: 999999999999, }; } this.tomeSettings = JSON.parse(localStorage.getItem(`${this.playerId}:QuesBS_tomeSettings`)); if (!this.tomeSettings && localStorage.getItem('QuesBS_tomeSettings')) { // Attempt migration from old settings this.tomeSettings = JSON.parse(localStorage.getItem('QuesBS_tomeSettings')); localStorage.setItem(`${this.playerId}:QuesBS_tomeSettings`, JSON.stringify(this.tomeSettings)); localStorage.removeItem('QuesBS_tomeSettings'); } else if(!this.tomeSettings) { this.tomeSettings = { goldKillTomesEquippedAmount: 0, useWeightSettings: false, weights: {}, thresholds: { reward: 999900, mobDebuff: 999900, character: 999900, characterWb: 999900, elementalConv: 999900, multiMob: 1, lifesteal: 1, actionSpeed: 1, mobSkip: 1, numGoodRolls: 1, numGoodRollsWb: 2, }, spaceThresholds: { reward: 6, mobDebuff: 6, character: 6, wb: 6, rare: 6, legendary: 6, }, hideMods: {}, disableRefreshOnHighlight: true, }; } // Legacy code updates, keep for now until bugs do not occur when it gets removed this.tomeSettings.useWeightSettings = this.tomeSettings.useWeightSettings ?? false; this.tomeSettings.weights = this.tomeSettings.weights ?? {}; this.tomeSettings.thresholds = this.tomeSettings.thresholds ?? { reward: this.tomeSettings.highlightReward ?? 99900, mobDebuff: this.tomeSettings.highlightMob ?? 99900, character: this.tomeSettings.highlightCharacter ?? 99900, characterWb: this.tomeSettings.highlightCharacterWb ?? 99900, elementalConv: this.tomeSettings.highlightElementalConv ?? 99900, multiMob: this.tomeSettings.highlightMultiMob ?? 1, lifesteal: this.tomeSettings.highlightLifesteal ?? 1, actionSpeed: this.tomeSettings.highlightActionSpeed ?? 1, mobSkip: this.tomeSettings.highlightMobSkip ?? 1, numGoodRolls: this.tomeSettings.numGoodRolls ?? 1, numGoodRollsWb: 2, } this.tomeSettings.spaceThresholds = this.tomeSettings.spaceThresholds ?? { reward: this.tomeSettings.spaceLimitReward ?? 6, mobDebuff: this.tomeSettings.spaceLimitMob ?? 6, character: this.tomeSettings.spaceLimitCharacter ?? 6, wb: this.tomeSettings.spaceLimitWb ?? 6, rare: this.tomeSettings.spaceLimitRare ?? 6, legendary: this.tomeSettings.spaceLimitLegendary ?? 6, } this.tomeSettings.hideMods = this.tomeSettings.hideMods ?? {}; this.tomeSettings.disableRefreshOnHighlight = this.tomeSettings.disableRefreshOnHighlight ?? true; } async getGameData() { //ULTIMATE POWER let tries = 10; //Get a reference to *all* the data the game is using, courtesy of Blah's exposed global object this.gameData = playerGeneralService; while(this.gameData === undefined && tries > 0) { //Power comes with a price; wait for it to load await new Promise(resolve => setTimeout(resolve, 500)) this.gameData = playerGeneralService; tries--; } if (tries <= 0) { console.log('QuesBS: Could not load gameData.'); } } async updateCatacombData() { /*** * Updates catacomb action timer in seconds and equipped tomes indicator ***/ // Wait until services load while(this.gameData?.playerCatacombService === undefined || this.gameData?.playerVillageService === undefined) { await new Promise(resolve => setTimeout(resolve, 200)); } const tomes = this.gameData.playerCatacombService.calculateTomeOverview(); // Calculate catacomb action speed in seconds let actionTimerSeconds = 24; if (this.gameData.playerCatacombService.actionData) { // If a catacomb is active, it will show how many seconds it has in an action actionTimerSeconds = this.gameData.playerCatacombService.actionData.actionSpeed; } else { // If a catacomb is inactive, actionData is undefined. Manually calculate const villageService = this.gameData.playerVillageService; let villageActionSpeedBoost; if (villageService?.isInVillage === true) { const level = villageService?.buildings?.observatory?.amount ?? 0; villageActionSpeedBoost = (Math.floor(level / 20) * Math.floor(level / 20 + 1) / 2 * 20 + (level % 20) * Math.floor(level / 20 + 1)) / 100; } else { villageActionSpeedBoost = 0; } actionTimerSeconds = 30 / (1 + villageActionSpeedBoost + tomes.speed / 100) + 0.2 } this.catacomb = { actionTimerSeconds: actionTimerSeconds, tomesAreEquipped: tomes.mobs > 0, } } async initPlayerData() { // Make sure gameData is loaded before initializing player data let loadingTries = 300; while ((!this.gameData || this.gameData.loadingService.loading) && loadingTries > 0) { if (!this.gameData) { this.getGameData(); } console.log('QuesBS: Waiting for game to load...'); await new Promise(resolve => setTimeout(resolve, 1000)); loadingTries--; } if (loadingTries <= 0) { console.log('QuesBS: Could not load player data. Please refresh or manually restart the script.'); return; } //Couldn't find an easier method to get quest completions than a POST request this.gameData.httpClient.post('/player/load/misc', {}).subscribe( val => { this.quest.questsCompleted = val.playerMiscData.quests_completed; this.playerId = val.playerMiscData.player_id; this.loadDataFromStorage(); }, response => console.log('QuesBS: POST request failure', response) ); await this.updateRefreshes(); if(this.gameData.playerVillageService?.isInVillage === true) { let villageService = this.gameData.playerVillageService; //Wait for service to load while(villageService === undefined) { await new Promise(resolve => setTimeout(resolve, 200)); villageService = this.gameData.playerVillageService; } this.quest.villageBold = villageService.strengths.bold.amount; this.quest.villageSize = villageService.general.members.length; this.quest.villageNumRefreshes = villageService.general.dailyQuestsBought + 5; this.quest.villageRefreshesUsed = villageService.general.dailyQuestsUsed; } //Can't be bothered to calculate it accurately using all 4 stats this.quest.baseStat = Math.min(15, this.gameData.playerStatsService?.strength * 0.0025); // Get catacomb data await this.updateCatacombData(); // Get kd exploration level for wb drops await this.updateKdInfo(); // Other misc player refreshing stuff this.gems = []; } async getPartyActions() { //A function to wait for party service to load //And also to abstract the horribly long method while(this.gameData?.partyService?.partyOverview?.partyInformation === undefined) { await new Promise(resolve => setTimeout(resolve, 200)); } return this.gameData.partyService.partyOverview.partyInformation[this.playerId].actions.daily_actions_remaining; } async updateRefreshes() { //Only made a load waiter because script was having issues with not loading while(this.gameData?.playerQuestService?.refreshesUsed === undefined) { await new Promise(resolve => setTimeout(resolve, 200)); } this.quest.numRefreshes = this.gameData.playerQuestService.refreshesBought + 20; this.quest.refreshesUsed = this.gameData.playerQuestService.refreshesUsed; } async updateVillageRefreshes() { let villageService = this.gameData.playerVillageService; this.quest.villageNumRefreshes = villageService.general.dailyQuestsBought + 5; this.quest.villageRefreshesUsed = villageService.general.dailyQuestsUsed; const maxBuildingLevel = Object.values(villageService.buildings).reduce((currMax, building) => Math.max(currMax, building.amount), 0); const buildingCost = villageService.calculateVillageBuildingUpgradeCost(maxBuildingLevel); this.quest.villageMaxResCost = buildingCost[1].cost * 4 + (buildingCost?.[5]?.cost ?? 0); } async updateKdInfo() { /** Only stores exploration information for wb drops */ let kdService = this.gameData.playerKingdomService; // Wait for game to load data while(kdService?.kingdomData?.explorations === undefined) { await new Promise(resolve => setTimeout(resolve, 200)); } this.kdExploLevel = kdService.kingdomData.explorations.level; } initObservers() { /** * Initialize observers which will be used to detect changes on * each specific page when it updates. */ let scriptObject = this; //mutation can't keep track of this this.personalQuestObserver = new MutationObserver(mutationsList => { scriptObject.handlePersonalQuest(mutationsList[0]); }); this.villageQuestObserver = new MutationObserver(mutationsList => { scriptObject.handleVillageQuest(mutationsList[0]); }); this.catacombObserver = new MutationObserver(mutationsList => { this.handleCatacombPage(mutationsList[0]); }); this.tomeObserver = new MutationObserver(mutationsList => { this.handleCatacombTomeStore(mutationsList[0]); }); this.wbDropsObserver = new MutationObserver(mutationsList => { this.handleWbChestOpening(mutationsList[0]); }); } async initPathDetection() { /** * Initializes the event trigger that will watch for changes in the * url path. This will allow us to determine which part of the * script to activate on each specific page. */ let router = this.gameData?.router //Wait for service to load while(router === undefined && router?.events === undefined) { await new Promise(resolve => setTimeout(resolve, 200)); router = this.gameData.router } this.gameData.router.events.subscribe(event => { if(event.navigationTrigger) this.handlePathChange(event.url); }); //Send a popup to player as feedback this.gameData.snackbarService.openSnackbar('QuesBS has been loaded.'); } async insertPlayerStatRatios(petDiv) { /* * Insert player stat ratios into the pet div by copy pasting from one of the existing * boxes. */ // Copy existing box to match the css style const statBoxElem = petDiv.children[1].children[2].cloneNode(true); statBoxElem.firstChild.innerText = 'Player stat ratios'; const statsBody = statBoxElem.children[1]; const playerStatsElem = document.querySelector('app-inventory-menu > div > div:nth-child(3)'); const statRatios = getStatRatios(playerStatsElem); // Insert the stat ratios for (let i = 0; i < statRatios.length; i++) { const row = statsBody.children[i].firstChild; row.children[1].innerText = `${playerStatsElem.children[i].children[1].innerText}`; const statRatioDiv = document.createElement('div'); statRatioDiv.innerText = `(${statRatios[i]})`; row.appendChild(statRatioDiv); } // Insert elem to be under the pet farm column petDiv.children[2].appendChild(statBoxElem); } async handlePathChange(url) { /** * Detects which page the player navigated to when the url path * has changed, then activates the observer for the page. */ const path = url.split('/').length == 2 ? url.split('/').slice(1) : url.split('/').slice(2); if(path.join() !== this.currentPath) { this.stopObserver(this.currentPath); } this.currentPath = path.join(); //Activate observer if on a specific page if(path[path.length - 1].toLowerCase() === 'quests' && path[0].toLowerCase() === 'actions') { //Observe personal quest page for updates let target = document.querySelector('app-actions'); //Sometimes the script attempts to search for element before it loads in while(!target) { await new Promise(resolve => setTimeout(resolve, 50)) target = document.querySelector('app-actions'); } this.personalQuestObserver.observe(target, { childList: true, subtree: true, attributes: false, }); //Sometimes there is no change observed for the initial page load, so call function await this.handlePersonalQuest({target: target}); } else if(path[path.length - 1].toLowerCase() === 'quests' && path[0].toLowerCase() === 'village') { //Observe village quest page for updates let target = document.querySelector('app-village'); //Sometimes the script attempts to search for element before it loads in while(!target) { await new Promise(resolve => setTimeout(resolve, 50)) target = document.querySelector('app-village'); } this.villageQuestObserver.observe(target, { childList: true, subtree: true, attributes: false, }); //Sometimes there is no change observed for the initial page load, so call function await this.handleVillageQuest({target: target}); } else if(path[path.length - 1].toLowerCase() === 'settings' && path[0].toLowerCase() === 'village') { //Insert our own settings box await this.insertVillageSettingsElem(); } else if(path[path.length - 1].toLowerCase() === 'catacomb' && path[0].toLowerCase() === 'catacombs') { this.updateCatacombData(); let target = document.querySelector('app-catacomb-main')?.firstChild; while(!target) { await new Promise(resolve => setTimeout(resolve, 200)) target = document.querySelector('app-catacomb-main').firstChild; } if (target.nodeName === '#comment') { // Active catacomb page // Only listen for change in active/inactive state this.catacombObserver.observe(target.parentElement, { childList: true, subtree: false, attributes: false, }); this.handleCatacombPage({target: target}); } else { this.catacombObserver.observe(target, { childList: true, subtree: true, attributes: false, }); } } else if (path[path.length - 1].toLowerCase() === 'tome_store' && path[0].toLowerCase() === 'catacombs') { await this.modifyTomeStorePage(); let target = $('app-catacomb-tome-store > div > div > div.base-scrollbar > div'); while(target.length < 1) { await new Promise(resolve => setTimeout(resolve, 200)) target = $('app-catacomb-tome-store > div > div > div.base-scrollbar > div'); } this.tomeObserver.observe(target[0], { childList: true, subtree: false, attributes: false }); this.handleCatacombTomeStore({target: target[0]}); } else if (path[path.length - 1].toLowerCase() === 'chests' && path[0].toLowerCase() === 'wb') { let target = $('app-game-world-boss-chests > div'); while(target.length < 1) { await new Promise(resolve => setTimeout(resolve, 200)) target = $('app-game-world-boss-chests > div'); } this.wbDropsObserver.observe(target[0], { childList: true, subtree: false, attributes: false }); } else if (path[path.length - 1].toLowerCase() === 'pets' && path[0].toLowerCase() === 'actions') { let target = $('app-actions-pets > .scrollbar > div > .d-flex'); while(target.length < 1) { await new Promise(resolve => setTimeout(resolve, 200)) target = $('app-actions-pets > .scrollbar > div > .d-flex'); } // Insert stat ratios on the pets page await this.insertPlayerStatRatios(target[0]); } else if (path[path.length - 1].toLowerCase() === 'gems' && path[0].toLowerCase() === 'inventory') { await this.insertFuseFrenzyButton(); } } async handlePersonalQuest(mutation) { /** * Handles a new update to the personal quests page. It loads in all * the extra quest information, which differs depending on an active or * non-active quest page view. */ //Filter out any unneeded mutations/updates to the page if(mutation?.addedNodes?.length < 1 || mutation?.addedNodes?.[0]?.localName === 'mat-tooltip-component' || mutation?.addedNodes?.[0]?.nodeName === 'TH' || mutation?.addedNodes?.[0]?.nodeName === 'TD' || mutation?.addedNodes?.[0]?.nodeName === '#text' || mutation?.addedNodes?.[0]?.className === 'mat-ripple-element' || mutation?.addedNodes?.[0]?.id === 'questInfoRow') { return; } //Modify the table used to hold quest information const questTable = mutation.target.parentElement.tagName === 'TABLE' ? mutation.target.parentElement : mutation.target.querySelector('table'); if(questTable) { let infoRow = null; //Add end time column to table this.addEndTimeColumn(questTable); const tableBody = questTable.children[1]; //There are two states: active quest and no quest if(tableBody.children.length > 2) {//No quest //Get the info row that goes at the bottom infoRow = await this.modifyQuestInfo(tableBody, false, false); } else if(tableBody.children.length > 0) { //Active quest //Update number of refreshes used, just in case await this.updateRefreshes(); infoRow = await this.modifyQuestInfo(tableBody, false, true); } else { return; } //Add an extra row for extra quest info if there isn't one already if(!document.getElementById('questInfoRow')) tableBody.appendChild(infoRow); } } async handleVillageQuest(mutation) { /** * Handles a new update to the village quests page. It loads in all * the extra quest information, which differs depending on an active or * non-active quest page view. */ //Filter out unneeded mutations/updates to page if(mutation?.addedNodes?.length < 1 || mutation?.addedNodes?.[0]?.nodeName === '#text' || mutation?.addedNodes?.[0]?.nodeName === 'TH' || mutation?.addedNodes?.[0]?.nodeName === 'TD' || mutation?.addedNodes?.[0]?.className === 'mat-ripple-element' || mutation?.addedNodes?.[0]?.id === 'questInfoRow') { return; } const questTable = mutation.target.parentElement.tagName === 'TABLE' ? mutation.target.parentElement : mutation.target.querySelector('table'); if(questTable) { await this.updateVillageRefreshes(); //Update for refreshes used this.addEndTimeColumn(questTable); //Add end time const tableBody = questTable.children[1]; //Add end time elems to the end time column if(tableBody.children.length > 2) { //Quest is not active await this.modifyQuestInfo(tableBody, true, false); } else { //Quest is active await this.modifyQuestInfo(tableBody, true, true); } //Add info text at the bottom of quest table const infoRow = document.createTextNode('End time is calculated assuming all members are active. The time is approximate and may not be accurate.' + `${this.quest.villageRefreshesUsed}/${this.quest.villageNumRefreshes} refreshes used.`); infoRow.id = 'questExplanation'; if(questTable.parentElement.lastChild.id !== 'questExplanation') { questTable.parentElement.appendChild(infoRow); } } } async handleCatacombPage(mutation) { /** * Handle an update on the catacomb page, and insert an end time into the page * for any selected catacomb. **/ if ( // skip unnecessary updates mutation?.addedNodes?.[0]?.localName === 'mat-tooltip-component' || mutation?.addedNodes?.[0]?.className === 'mat-ripple-element' || mutation?.addedNodes?.[0]?.nodeName === '#text' || mutation?.addedNodes?.[0]?.id === 'catacombEndTime' ) { return; } const mainView = document.querySelector('app-catacomb-main'); //Check if active or inactive view if (mainView.firstChild.nodeName === '#comment') { // Active view const parentElement = mainView.firstElementChild.firstChild.firstChild.firstChild; const secondsLeft = parseNumber(parentElement.children[1].innerText); // Use api data const totalMobs = this.gameData.playerCatacombService.actionData.catacombStats.mobCount; const mobsKilled = this.gameData.playerCatacombService.actionData.catacombStats.killCount; // Create the end time ele to insert into const endTimeEle = document.getElementById('catacombEndTime') ?? document.createElement('div'); endTimeEle.id = 'catacombEndTime'; endTimeEle.setAttribute('class', 'h5'); endTimeEle.innerText = `| End time: ${getCatacombEndTime(totalMobs - mobsKilled, this.catacomb.actionTimerSeconds, secondsLeft)}`; parentElement.appendChild(endTimeEle); } else { // Inactive view const mobOverviewEle = mainView.firstChild.children[1].firstChild.firstChild; const totalMobs = parseNumber(mobOverviewEle.firstChild.children[1].firstChild.children[11].children[1].innerText); const cataTierSelectionEle = mobOverviewEle.children[1]; // Create the end time ele to insert into const endTimeEle = document.getElementById('catacombEndTime') ?? document.createElement('div'); endTimeEle.id = 'catacombEndTime'; endTimeEle.innerText = `End time (local): ${getCatacombEndTime(totalMobs, this.catacomb.actionTimerSeconds)}`; cataTierSelectionEle.appendChild(endTimeEle); // Create tooltips for gold/hr and emblems/hr const goldEle = mobOverviewEle.firstChild.children[1].firstChild.children[9].children[1]; const boostedGoldPerKill = parseNumber(goldEle.innerText); const goldHr = boostedGoldPerKill / this.catacomb.actionTimerSeconds * 3600; goldEle.parentElement.setAttribute('title', `${goldHr.toLocaleString(undefined, {maximumFractionDigits:2})}/Hr`); const emblemsEle = mobOverviewEle.firstChild.children[1].firstChild.children[10].children[1]; const emblemsHr = parseNumber(emblemsEle.innerText) / totalMobs / this.catacomb.actionTimerSeconds * 3600; emblemsEle.parentElement.setAttribute('title', `${emblemsHr.toLocaleString(undefined, {maximumFractionDigits:2})}/Hr`); // Highlight start button if tomes are equipped const goldPerKillEle = mutation.target.parentElement.parentElement?.previousSibling?.children?.[9]?.firstElementChild; if (!goldPerKillEle) return; // Early return if element cannot be found, since mutations can come from anything const baseGoldPerKill = parseNumber(goldPerKillEle.innerText); const startCataButton = mobOverviewEle.nextSibling.firstChild; if (this.catacomb.tomesAreEquipped && baseGoldPerKill < this.tomeSettings.goldKillTomesEquippedAmount) { startCataButton.style.boxShadow = '0px 0px 12px 7px red'; startCataButton.style.color = 'red'; } else { startCataButton.style.boxShadow = 'none'; startCataButton.style.color = ''; } } } async handleCatacombTomeStore(mutation) { /** * Add highlights around tomes with good boosts and obscures bad tomes * Credit to Ender for code collaboration and fading out tomes * **/ if ( // skip unnecessary updates mutation?.addedNodes?.[0]?.localName === 'mat-tooltip-component' || mutation?.addedNodes?.[0]?.className === 'mat-ripple-element' || mutation?.addedNodes?.[0]?.nodeName === '#text' || mutation?.addedNodes?.[0]?.id === 'highlight' ) { return; } // Get store element and tome store data const tomeElements = $('app-catacomb-tome-store > div > div > div.base-scrollbar > div > div'); let tomes = this.gameData.playerCatacombService?.tomeStore; while (this.gameData.playerCatacombService === undefined || tomes === undefined) { await new Promise(resolve => setTimeout(resolve, 200)) tomes = this.gameData.playerCatacombService?.tomeStore; } // Put an id on the first tome of the store to mark it as "processed" tomeElements[0].id = 'highlight'; // Get the refresh button for disabling it const refreshButton = $('app-catacomb-tome-store > div > div > div.my-auto > div > button')[0]; refreshButton.style.touchAction = 'manipulation'; // Disable double tap zoom for mobile when tapping the button // For each tome (loop by index), check if tome has good modifiers. for (let i = 0; i < tomes.length; i++) { const tomeMods = tomes[i]; const tomeElement = tomeElements[i].firstChild; let shouldFadeTome = true; let highlightIncome = false; if (this.tomeSettings?.useWeightSettings) { // Create row after tome mods to display the power per space values const displayEle = tomeElement.querySelector(`#perspacedisplay-${tomeMods.id}`) ?? document.createElement('div'); displayEle.id = `perspacedisplay-${tomeMods.id}`; displayEle.className = 'd-flex justify-content-between ng-star-inserted'; tomeElement.appendChild(displayEle, tomeElement.nextSibling.firstChild); if (this.checkTomeIncomePower(tomeMods, displayEle)) { // Displays income power as well shouldFadeTome = false; highlightIncome = true; } if (this.checkTomeWBPower(tomeMods, displayEle)) { // Displays wb power as well shouldFadeTome = false; } } else { if (this.checkTomeIncomeMeetsThresholds(tomeMods, tomeElement)) { shouldFadeTome = false; highlightIncome = true; } if (this.checkTomeWBMeetsThresholds(tomeMods, tomeElement)) { shouldFadeTome = false; } } // Fade out tomes that didn't meet requirements if (shouldFadeTome) { tomeElement.style.color = "rgba(255, 255, 255, 0.4)"; [...tomeElement.children].forEach((child) => { child.style.opacity = "0.4"; }); } else { if (this.tomeSettings.disableRefreshOnHighlight) { // Briefly disable the refresh button refreshButton.disabled = true; refreshButton.className = 'mat-focus-indicator mat-stroked-button mat-button-base'; document.querySelector('#stopScrollDiv').focus({preventScroll: true}); // Prevent spacebar from scrolling down setTimeout((button) => { button.disabled = false; button.className = 'mat-focus-indicator mat-raised-button mat-button-base'; button.focus({preventScroll: true}); }, 1600, refreshButton); } if (highlightIncome) { tomeElement.parentElement.style.boxShadow = '0 0 30px 15px #48abe0'; } else { tomeElement.parentElement.style.boxShadow = '0 0 30px 15px green'; } } // Hide modifiers on tomes according to the settings const modifierOrder = [ 'tomeName', 'spaceReq', 'addedMobs', 'reward', 'mobDebuff', 'character', 'waterResistance', 'thunderResistance', 'fireResistance', 'meleeResistance', 'rangedResistance', 'elementalConversion', 'fortifyReduction' ]; for (let i = 2; i < tomeElement.children.length; i++) { const modifierEle = tomeElement.children[i]; if (this.tomeSettings.hideMods[modifierOrder[i]]) { modifierEle.style.setProperty('display', 'none', 'important'); } else { modifierEle.style.display = ''; } } } } checkTomeIncomePower(tomeMods, powerDisplayEle) { /** * Returns true if the tomeMods are weighted and meet the thresholds according to the tome settings for income. * It also displays the income power on the tome using the powerDisplayEle elememt */ let incomePerSpace = 0; incomePerSpace += (tomeMods.multi_mob ?? 0) / 100 * (this.tomeSettings.weights.multiMob ?? 0); incomePerSpace += (tomeMods.character_multiplier ?? 0) / 100 * (this.tomeSettings.weights.character ?? 0); incomePerSpace += (tomeMods.speed ?? 0) / 100 * (this.tomeSettings.weights.actionSpeed ?? 0); incomePerSpace += (tomeMods.skip ?? 0) / 100 * (this.tomeSettings.weights.mobSkip ?? 0); incomePerSpace += (tomeMods.lifesteal ?? 0) / 100 * (this.tomeSettings.weights.lifesteal ?? 0); incomePerSpace += (tomeMods.reward_multiplier ?? 0) / 100 * (this.tomeSettings.weights.reward ?? 0); incomePerSpace += (tomeMods.mob_multiplier ?? 0) / 100 * (this.tomeSettings.weights.mobDebuff ?? 0); incomePerSpace /= tomeMods.space_requirement; // Display the income power per space in the powerDisplayEle const incomePerSpaceEle = powerDisplayEle.querySelector(`#incomeperspace-${tomeMods.id}`) ?? document.createElement('div'); incomePerSpaceEle.id = `incomeperspace-${tomeMods.id}`; incomePerSpaceEle.innerText = `Income: ${Math.round(incomePerSpace).toLocaleString()}`; powerDisplayEle.appendChild(incomePerSpaceEle); // Check if (incomePerSpace >= this.tomeSettings.weights.incomePerSpaceThreshold) { incomePerSpaceEle.style.color = 'gold'; return true; } return false; } checkTomeWBPower(tomeMods, powerDisplayEle) { /** * Returns true if the tomeMods are weighted and meet the thresholds according to the tome settings for world boss. * It also displays the WB power on the tome using the powerDisplayEle elememt */ let wbPowerPerSpace = (tomeMods.character_multiplier ?? 0) / 100.0 * (this.tomeSettings.weights.wbCharacter ?? 0); wbPowerPerSpace += (tomeMods.elemental_conversion ?? 0) / 100.0 * (this.tomeSettings.weights.wbElementalConv ?? 0); wbPowerPerSpace /= tomeMods.space_requirement; // Display the WB power per space in the powerDisplayEle const wbPowerPerSpaceEle = powerDisplayEle.querySelector(`#wbpowerperspace-${tomeMods.id}`) ?? document.createElement('div'); wbPowerPerSpaceEle.id = `wbpowerperspace-${tomeMods.id}`; wbPowerPerSpaceEle.innerText = `WB: ${Math.round(wbPowerPerSpace).toLocaleString()}`; powerDisplayEle.appendChild(wbPowerPerSpaceEle); if (wbPowerPerSpace >= this.tomeSettings.weights.wbPowerPerSpaceThreshold) { wbPowerPerSpaceEle.style.color = 'forestgreen'; return true; } return false; } checkTomeIncomeMeetsThresholds(tomeMods, tomeElement) { /** * Returns true if given tomes meets thresholds accored to the stored tome settings for income * Also highlight the tomeElement if threshold is met */ let sumGoodRolls = 0; // Check each important mods: reward, character, mob, lifesteal, multi mob, action speed, mob skip // For each mod, if it meets the settings highlight the mod and increment the number of rolls if ( this.tomeSettings.thresholds.reward > 0 && tomeMods.reward_multiplier >= this.tomeSettings.thresholds.reward && tomeMods.space_requirement <= this.tomeSettings.spaceThresholds.reward ) { const isDouble = tomeMods.reward_multiplier >= this.tomeSettings.thresholds.reward * 2; tomeElement.children[3].style.border = `${isDouble ? 'thick' : '2px'} solid`; tomeElement.children[3].style.borderColor = tomeElement.children[3].firstChild.style.color ?? 'gold'; sumGoodRolls += Math.floor(tomeMods.reward_multiplier / this.tomeSettings.thresholds.reward); } if ( this.tomeSettings.thresholds.mobDebuff > 0 && tomeMods.mob_multiplier >= this.tomeSettings.thresholds.mobDebuff && tomeMods.space_requirement <= this.tomeSettings.spaceThresholds.mobDebuff ) { const isDouble = tomeMods.mob_multiplier >= this.tomeSettings.thresholds.mobDebuff * 2; tomeElement.children[4].style.border = `${isDouble ? 'thick' : '2px'} solid`; tomeElement.children[4].style.borderColor = tomeElement.children[4].firstChild.style.color ?? 'white'; sumGoodRolls += Math.floor(tomeMods.mob_multiplier / this.tomeSettings.thresholds.mobDebuff); } if ( this.tomeSettings.thresholds.character > 0 && tomeMods.character_multiplier >= this.tomeSettings.thresholds.character && tomeMods.space_requirement <= this.tomeSettings.spaceThresholds.character ) { const isDouble = tomeMods.character_multiplier >= this.tomeSettings.thresholds.character * 2; tomeElement.children[5].style.border = `${isDouble ? 'thick' : '2px'} solid`; tomeElement.children[5].style.borderColor = tomeElement.children[5].firstChild.style.color ?? 'white'; sumGoodRolls += Math.floor(tomeMods.character_multiplier / this.tomeSettings.thresholds.character); } if ( this.tomeSettings.thresholds.lifesteal > 0 && tomeMods.lifesteal >= this.tomeSettings.thresholds.lifesteal && tomeMods.space_requirement <= this.tomeSettings.spaceThresholds.rare ) { sumGoodRolls += Math.floor(tomeMods.lifesteal / this.tomeSettings.thresholds.lifesteal); } if ( this.tomeSettings.thresholds.multiMob > 0 && tomeMods.multi_mob >= this.tomeSettings.thresholds.multiMob && tomeMods.space_requirement <= this.tomeSettings.spaceThresholds.rare ) { sumGoodRolls += Math.floor(tomeMods.multi_mob / this.tomeSettings.thresholds.multiMob); } if ( this.tomeSettings.thresholds.actionSpeed > 0 && tomeMods.speed >= this.tomeSettings.thresholds.actionSpeed && tomeMods.space_requirement <= this.tomeSettings.spaceThresholds.legendary ) { sumGoodRolls += Math.floor(tomeMods.speed / this.tomeSettings.thresholds.actionSpeed); } if ( this.tomeSettings.thresholds.mobSkip > 0 && tomeMods.skip >= this.tomeSettings.thresholds.mobSkip && tomeMods.space_requirement <= this.tomeSettings.spaceThresholds.legendary ) { sumGoodRolls += Math.floor(tomeMods.skip / this.tomeSettings.thresholds.mobSkip); } return sumGoodRolls >= this.tomeSettings.thresholds.numGoodRolls; } checkTomeWBMeetsThresholds(tomeMods, tomeElement) { /** * Returns true if given tomes meets thresholds accored to the stored tome settings for world boss * Also highlight the tomeElement if threshold is met */ let sumRolls = 0; if ( this.tomeSettings.thresholds.elementalConv > 0 && tomeMods.elemental_conversion >= this.tomeSettings.thresholds.elementalConv && tomeMods.space_requirement <= this.tomeSettings.spaceThresholds.wb ) { const isDoubleElemental = tomeMods.elemental_conversion >= this.tomeSettings.thresholds.elementalConv * 2; sumRolls += Math.floor(tomeMods.elemental_conversion / this.tomeSettings.thresholds.elementalConv); tomeElement.children[11].style.border = `${isDoubleElemental ? 'thick' : '1px'} solid`; tomeElement.children[11].style.borderColor = 'forestgreen'; } if ( this.tomeSettings.thresholds.characterWb > 0 && tomeMods.character_multiplier >= this.tomeSettings.thresholds.characterWb && tomeMods.space_requirement <= this.tomeSettings.spaceThresholds.wb ) { const isDoubleCharacter = tomeMods.character_multiplier >= this.tomeSettings.thresholds.characterWb * 2; sumRolls += Math.floor(tomeMods.character_multiplier / this.tomeSettings.thresholds.characterWb); tomeElement.children[5].style.border = `${isDoubleCharacter ? 'thick' : '1px'} solid`; tomeElement.children[5].style.borderColor = 'forestgreen'; } return sumRolls >= this.tomeSettings.thresholds.numGoodRollsWb; } async handleWbChestOpening(mutation) { /** * Highlight drops that are desirable * - Gems over the kd level * - Descriptions with max depth 31+ * - Equipment with depth 31+ **/ // Check if first time opening chests on page if (mutation?.addedNodes?.[0]?.innerText && mutation.addedNodes[0].innerText.startsWith('After')) { // Change observer to listen to subsequent chest openings let target = document.querySelector('app-game-world-boss-chest-drops'); this.wbDropsObserver.disconnect(); this.wbDropsObserver.observe(target, { childList: true, subtree: false, attributes: false }); } // Get list of drops const dropsCategories = document.querySelector('app-game-world-boss-chest-drops').children; for (const category of dropsCategories) { const text = category.innerText.split(' '); const dropType = text[text.length - 1].toLowerCase(); if (dropType === 'gem' || dropType === 'description' || dropType === 'item') { // They are grouped together in an inner list, so extract the inner list const dropList = category.firstElementChild.children; for (const drop of dropList) { const text = drop.innerText.split(' '); // Additional filters if (dropType === 'gem' && parseNumber(text[1]) < this.kdExploLevel) { // Gem has to be higher level than kd exploration level continue; } else if (dropType === 'description' && parseNumber(text[1].split('-')[1]) <= 30) { // Description has to be max depth 31+ continue; } else if (dropType === 'item' && parseNumber(text[1]) < 31) { // Fighter item must be depth 31+ continue; } // Highlight the element drop.style.backgroundColor = 'darkblue'; } } } } stopObserver(pathname) { const stop = { 'actions,quests': () => this.personalQuestObserver.disconnect(), 'village,quests': () => this.villageQuestObserver.disconnect(), 'catacombs,catacomb': () => this.catacombObserver.disconnect(), 'catacombs,tome_store': () => this.tomeObserver.disconnect(), 'wb,chests': () => this.wbDropsObserver.disconnect(), 'portal': () => { setTimeout(this.initPlayerData.bind(this), 2000)}, 'inventory,gems': () => { this.gems = [] }, } if(stop[pathname]) { stop[pathname](); } } getStatReward() { /** * Returns the possible max and min values for stat quests */ return { max: Math.round((this.quest.questsCompleted/300+this.quest.baseStat+22.75)*(1+this.quest.villageBold*2/100)*1.09), min: Math.round((this.quest.questsCompleted/300+this.quest.baseStat+8.5)*(1+this.quest.villageBold*2/100)*1.09), } } async getQuestInfoElem(actionsNeeded) { /** * Returns the info row used for active personal quest page */ const partyActions = await this.getPartyActions(); let row = document.createElement('tr'); const date = new Date(); //actionsNeeded * 6000 = actions * 6 sec per action * 1000 milliseconds const finishPartyTime = new Date(date.getTime() + (actionsNeeded + partyActions) * 6000).toLocaleTimeString('en-GB').match(/\d\d:\d\d/)[0]; const info = ['',`${this.quest.refreshesUsed}/${this.quest.numRefreshes} refreshes used`, '', actionsNeeded >= 0 ? `End time (local time) with ${partyActions} party actions: ${finishPartyTime}`: '']; let htmlInfo = ''; for (let text of info) { htmlInfo += `<td>${text}</td>` } row.innerHTML = htmlInfo; row.id = 'questInfoRow'; return row; } getTimeElem(actionsNeeded, className, isVillage=true) { /** * Returns an element used to describe the end time for each quest, used for * the end time column. It has styled CSS through the className, and the * time calculation differs for village vs personal. If there are an * invalid number of actionsNeeded, the time is N/A. */ const cell = document.createElement('td'); if(actionsNeeded > 0) { const date = new Date(); const numPeople = isVillage ? this.quest.villageSize : 1; //actionsNeeded * 6 sec per action * 1000 milliseconds / numPeople const finishTime = new Date(date.getTime() + actionsNeeded * 6000 / numPeople).toLocaleTimeString('en-GB').match(/\d\d:\d\d/)[0]; cell.innerText = finishTime; } else { cell.innerText = 'N/A'; } cell.setAttribute('class', className); return cell; } getQuestRatioInfo() { //Return info row used for inactive personal quests let row = document.createElement('tr'); const stat = this.getStatReward(); const avg = (stat.max/this.quest.minActions + stat.min/this.quest.maxActions) / 2; const info = ['Possible stat ratios, considering quests completed & village bold:', `Worst ratio: ${(stat.min/this.quest.maxActions).toFixed(3)}`, `Avg ratio: ${(avg).toFixed(3)}`, `Best Ratio: ${(stat.max/this.quest.minActions).toFixed(3)}`, '' ]; let htmlInfo = ''; for (let text of info) { htmlInfo += `<td>${text}</td>` } row.innerHTML = htmlInfo; row.setAttribute('class', 'mat-row cdk-row ng-star-inserted'); row.id = 'questInfoRow'; return row; } addEndTimeColumn(tableElem) { //Given a table element, add a new column for end time and add times to each row if(tableElem === undefined) return; //Add header title for the column if(tableElem?.firstChild?.firstChild?.nodeType !== 8 && (tableElem.firstChild.firstChild.children?.[3]?.id !== 'endTimeHeader' //Inactive vs active quest && tableElem.firstChild.firstChild.children?.[4]?.id !== 'endTimeHeader')) { const header = document.createElement('th'); header.innerText = 'End Time (local time)'; header.id = 'endTimeHeader'; header.setAttribute('class', tableElem.firstChild.firstChild?.firstChild?.className ?? 'mat-header-cell cdk-header-cell cdk-column-current mat-column-current ng-star-inserted'); tableElem.firstChild.firstChild.appendChild(header); } } async modifyQuestInfo(tableBody, isVillage, isActiveQuest) { /* Returns info row because I suck at structure ** Also inserts the end time for each quest */ //First, determine if quest is active if(isActiveQuest && tableBody.children[0]) { //If it is, parse the text directly to get the end time const row = tableBody.children[0]; const objectiveElemText = row?.children[1].innerText.split(' '); let timeElem; if(objectiveElemText[3].toLowerCase() === 'actions' || objectiveElemText[3].toLowerCase() === 'survived') { const actionsDone = parseNumber(objectiveElemText[0]); const objective = parseNumber(objectiveElemText[2]); const reward = row.children[2].innerText.split(' '); let actionsNeeded = -1; //Special case: Party action quest (because it has 7 sec timer) if(row.children[2].innerText.split(' ')[1].toLowerCase() === 'party') { actionsNeeded = (objective - actionsDone) * 7 / 6; } else { actionsNeeded = objective - actionsDone; } timeElem = this.getTimeElem(actionsNeeded, row.firstChild.className, isVillage); row.appendChild(timeElem); //Add ratios if(reward[1].toLowerCase() === 'gold') { const ratio = Math.round(parseInt(reward[0]) / objective * 600).toLocaleString(); row.children[2].innerText = `${row.children[2].innerText} (${ratio} gold/hr)`; } else if(!isVillage) { const ratio = (parseInt(reward[0]) / objective).toFixed(3); row.children[2].innerText = `${row.children[2].innerText} (${ratio})`; } return await this.getQuestInfoElem(actionsNeeded); } else if(objectiveElemText[3].toLowerCase() === 'base') { //Special case: Exp reward quest const goldCollected = parseNumber(objectiveElemText[0]); const objective = parseNumber(objectiveElemText[2]); const currentMonster = this.gameData.playerActionService.selectedMonster; const baseGoldPerAction = 8 + 2 * currentMonster; const actionsNeeded = Math.ceil((objective - goldCollected) / baseGoldPerAction); // Insert end time timeElem = this.getTimeElem(actionsNeeded, row.firstChild.className, isVillage); row.appendChild(timeElem); //Add ratio const expNeeded = this.gameData.playerLevelsService.battling.exp.needed; const reward = parseNumber(row.children[2].innerText.split(' ')[0].replace(/,/g, '')) / 100; const ratio = Math.round((expNeeded * reward) / (objective / baseGoldPerAction)).toLocaleString(); row.children[2].innerText = `${row.children[2].innerText} (${ratio} exp/action)`; // Replace exp requirement with action requirement const actionsDone = Math.floor(goldCollected / baseGoldPerAction).toLocaleString(); const actionsLeft = Math.ceil(objective / baseGoldPerAction).toLocaleString(); row.children[1].innerText = `${actionsDone} / ${actionsLeft} actions (does not update)`; return await this.getQuestInfoElem(actionsNeeded); } else { timeElem = this.getTimeElem(-1, row.firstChild.className, isVillage); row.appendChild(timeElem); return await this.getQuestInfoElem(-1); } } else if(isVillage && tableBody.children[0]) { const refreshButton = $('app-village-quests > div > div > div.mt-3 > button')[0]; refreshButton.style.touchAction = 'manipulation'; // Disable double tap zoom for mobile when tapping the button //Get village quests for(let i = 0; i < tableBody.children.length; i++) { let row = tableBody.children[i]; const objectiveText = row.children[1].innerText.split(' '); let timeElem = null; let meetsHighlightReq = false; if(objectiveText[1] === 'actions') { //Add border if there's a str point reward or it meets the minResAction threshold const rewardText = row.children[2].innerText.split(' '); const reward = rewardText[1]; if(reward === 'strength' && parseNumber(objectiveText[0]) <= this.villageSettings.strActions) { meetsHighlightReq = true; } else if (parseNumber(rewardText[0]) / parseNumber(objectiveText[0]) >= this.villageSettings.resActionRatio) { // res reward ratio meets threshold setting meetsHighlightReq = true; } if (meetsHighlightReq) { row.children[2].style.border = 'inset'; if (this.tomeSettings.disableRefreshOnHighlight) { refreshButton.disabled = true; refreshButton.className = 'mat-focus-indicator mr-2 mat-stroked-button mat-button-base'; document.querySelector('#stopScrollDiv').focus({preventScroll: true}); // Prevent spacebar from scrolling down setTimeout((button) => { button.disabled = false; button.className = 'mat-focus-indicator mr-2 mat-raised-button mat-button-base'; button.focus({preventScroll: true}); }, 1200, refreshButton); } } //Insert end time const objective = parseNumber(objectiveText[0]); timeElem = this.getTimeElem(objective, row.firstChild.className, true); } else { timeElem = this.getTimeElem(-1, row.firstChild.className, true); } row.appendChild(timeElem); } return; } else if(tableBody.children[0]) { //personal not active quests const availableQuests = this.gameData.playerQuestService.questArray; //Go through each quest and update row accordingly for(let i = 0; i < availableQuests.length; i++) { const row = tableBody.children[i]; let actionsNeeded = -1; if(availableQuests[i].type === 'swordsman' || availableQuests[i].type === 'tax' || availableQuests[i].type === 'gems' || availableQuests[i].type === 'spell') { //Above are the quests that require actions to be done actionsNeeded = parseNumber(availableQuests[i].objective.split(' ')[0]); } else if(availableQuests[i].type === 'treasure') { actionsNeeded = parseNumber(availableQuests[i].objective.split(' ')[0]); //Insert a gold ratio const reward = parseNumber(availableQuests[i].reward.split(' ')[0]); const ratio = Math.round(reward / actionsNeeded * 600).toLocaleString(); row.children[1].innerText = `${row.children[1].innerText} (${ratio} gold/hr)`; } else if(availableQuests[i].type === 'slow') { //Convert 7 second actions to 6 second actions actionsNeeded = parseNumber(availableQuests[i].objective.split(' ')[0]) * 7 / 6; } else if(availableQuests[i].type === 'friend') { //Base gold objective const goldObjective = parseNumber(availableQuests[i].objective.split(' ')[0]); const currentMonster = this.gameData.playerActionService.selectedMonster; actionsNeeded = Math.ceil(goldObjective / (8 + 2 * currentMonster)); //Insert a exp ratio const reward = parseNumber(row.children[1].innerText.split(' ')[0]); const ratio = Math.round(reward / actionsNeeded).toLocaleString(); row.children[1].innerText = `${row.children[1].innerText} (${ratio} exp/action)`; // Convert gold requirement to action requirement row.children[0].innerText = `${actionsNeeded.toLocaleString()} actions`; } if(row.id !== 'questInfoRow'){ const timeElem = this.getTimeElem(actionsNeeded, row.firstChild.className, false); row.appendChild(timeElem); } } return this.getQuestRatioInfo(); //The bottom row that contains extra info } } async insertVillageSettingsElem() { /** * Inserts a custom settings box into the village settings page */ //Get settings page contents let settingsOverview = document.querySelector('app-village-settings'); while(!settingsOverview) { await new Promise(resolve => setTimeout(resolve, 50)); settingsOverview = document.querySelector('app-village-settings'); } //Clone a copy of the armory settings to match the css style const questSettings = settingsOverview.firstChild.children[1].cloneNode(true); questSettings.style.width = '35%'; questSettings.style.maxWidth = ''; //Modify to our liking questSettings.firstChild.children[3].remove(); questSettings.firstChild.children[2].remove(); questSettings.firstChild.firstChild.innerText = 'QuesBS Highlight Quest'; questSettings.firstChild.children[1].firstChild.innerText = 'Max actions for strength point'; questSettings.firstChild.children[1].children[1].id = 'actionsLimitSetting'; questSettings.firstChild.children[1].children[1].style.width = '50%'; questSettings.firstChild.children[1].children[1].firstChild.value = this.villageSettings.strActions; questSettings.firstChild.children[1].children[1].firstChild.style.width = '6em'; // Clone for extra rows const resActionSetting = questSettings.firstChild.children[1].cloneNode(true); resActionSetting.firstChild.innerText = 'Min ratio for res/action quests'; resActionSetting.children[1].id = 'minResActionSetting'; resActionSetting.children[1].firstChild.value = this.villageSettings.resActionRatio ?? 999999999999; resActionSetting.children[1].firstChild.style.width = '9em'; const taxObjectiveSetting = questSettings.firstChild.children[1].cloneNode(true); taxObjectiveSetting.firstChild.innerText = 'Max tax objective % (0.5~1.25)'; taxObjectiveSetting.children[1].id = 'taxObjectiveSetting'; taxObjectiveSetting.children[1].firstChild.value = this.villageSettings.taxObjectivePercent ?? 0; // Insert const saveButton = questSettings.firstChild.children[2]; saveButton.disabled = false; saveButton.className = 'mat-focus-indicator mat-raised-button mat-button-base'; // Make it look enabled in case it's disabled questSettings.firstChild.insertBefore(resActionSetting, saveButton); questSettings.firstChild.insertBefore(taxObjectiveSetting, saveButton); saveButton.firstChild.firstChild.innerText = 'Save QuesBS Quests'; //Add a save function for button saveButton.firstChild.onclick = () => { const newActions = parseNumber(document.getElementById('actionsLimitSetting').firstChild.value); const newResActions = parseNumber(document.getElementById('minResActionSetting').firstChild.value); const newTaxObjectivePercent = parseNumber(document.getElementById('taxObjectiveSetting').firstChild.value); //Data validation if(isNaN(newActions) || isNaN(newTaxObjectivePercent) || isNaN(newResActions)) { this.gameData.snackbarService.openSnackbar('Error: Value should be a number'); //feedback popup } else if (newTaxObjectivePercent < 0 || newTaxObjectivePercent > 1.25) { this.gameData.snackbarService.openSnackbar('Error: Tax % should be between 0 and 1.25'); } else { this.villageSettings.strActions = newActions; this.villageSettings.taxObjectivePercent = newTaxObjectivePercent; this.villageSettings.resActionRatio = newResActions; localStorage.setItem(`${this.playerId}:QuesBS_villageSettings`, JSON.stringify(this.villageSettings)); this.gameData.snackbarService.openSnackbar('Settings saved successfully'); //feedback popup } } settingsOverview.appendChild(questSettings); } async modifyTomeStorePage() { /** * Inserts a custom popup menu for tome settings */ //Get store page contents let tomeStoreOverview = document.querySelector('app-catacomb-tome-store'); while(!tomeStoreOverview) { await new Promise(resolve => setTimeout(resolve, 50)); tomeStoreOverview = document.querySelector('app-catacomb-tome-store'); } // Create settings menu const settings = document.createElement('div'); settings.id = 'highlightTomeSettings'; settings.style.margin = '1rem'; settings.style.position = 'relative'; settings.innerHTML = GM_getResourceText('settingsMenu'); const openTomeSettingsbutton = document.createElement('button'); openTomeSettingsbutton.id = 'openTomeSettingsButton'; openTomeSettingsbutton.className = 'mat-focus-indicator mat-raised-button mat-button-base'; openTomeSettingsbutton.innerText = 'QuesBS Tome Settings'; settings.insertBefore(openTomeSettingsbutton, settings.childNodes[0]); const topStoreBar = tomeStoreOverview.firstChild.firstChild; topStoreBar.insertBefore(settings, topStoreBar.firstChild); // Display correct settings const settingsContainer = settings.childNodes[1]; if (this.tomeSettings?.useWeightSettings) { settingsContainer.querySelector('#tomeWeightSettingsBlock').style.display = 'block'; settingsContainer.querySelector('#thresholdSettingsBlock').style.display = 'none'; } // Fill in input values const d = 99900; // Default value settingsContainer.querySelector('#rewardHighlightSetting').value = ((this.tomeSettings.thresholds.reward ?? d) / 100).toFixed(2); settingsContainer.querySelector('#mobHighlightSetting').value = ((this.tomeSettings.thresholds.mobDebuff ?? d) / 100).toFixed(2); settingsContainer.querySelector('#characterHighlightSetting').value = ((this.tomeSettings.thresholds.character ?? d) / 100).toFixed(2); settingsContainer.querySelector('#characterWbHighlightSetting').value = ((this.tomeSettings.thresholds.characterWb ?? d) / 100).toFixed(2); settingsContainer.querySelector('#elementalConvHighlightSetting').value = ((this.tomeSettings.thresholds.elementalConv ?? d) / 100).toFixed(2); settingsContainer.querySelector('#multiMobHighlightSetting').value = ((this.tomeSettings.thresholds.multiMob ?? d) / 100).toFixed(2); settingsContainer.querySelector('#lifestealHighlightSetting').value = ((this.tomeSettings.thresholds.lifesteal ?? d) / 100).toFixed(2); settingsContainer.querySelector('#actionSpeedHighlightSetting').value = ((this.tomeSettings.thresholds.actionSpeed ?? d) / 100).toFixed(2); settingsContainer.querySelector('#mobSkipHighlightSetting').value = ((this.tomeSettings.thresholds.mobSkip ?? d) / 100).toFixed(2); settingsContainer.querySelector('#rewardSpaceSetting').value = this.tomeSettings.spaceThresholds.reward ?? 6; settingsContainer.querySelector('#mobSpaceSetting').value = this.tomeSettings.spaceThresholds.mobDebuff ?? 6; settingsContainer.querySelector('#characterSpaceSetting').value = this.tomeSettings.spaceThresholds.character ?? 6; settingsContainer.querySelector('#wbSpaceSetting').value = this.tomeSettings.spaceThresholds.wb ?? 9; settingsContainer.querySelector('#rareSpaceSetting').value = this.tomeSettings.spaceThresholds.rare ?? 9; settingsContainer.querySelector('#legendarySpaceSetting').value = this.tomeSettings.spaceThresholds.legendary ?? 9; settingsContainer.querySelector('#numGoodRolls').value = this.tomeSettings.thresholds.numGoodRolls ?? 1; settingsContainer.querySelector('#numGoodRollsWb').value = this.tomeSettings.thresholds.numGoodRollsWb ?? 2; settingsContainer.querySelector('#actionSpeedWeight').value = this.tomeSettings.weights.actionSpeed ?? 0; settingsContainer.querySelector('#mobSkipWeight').value = this.tomeSettings.weights.mobSkip ?? 0; settingsContainer.querySelector('#multiMobWeight').value = this.tomeSettings.weights.multiMob ?? 0; settingsContainer.querySelector('#lifestealWeight').value = this.tomeSettings.weights.lifesteal ?? 0; settingsContainer.querySelector('#rewardWeight').value = this.tomeSettings.weights.reward ?? 0; settingsContainer.querySelector('#mobDebuffWeight').value = this.tomeSettings.weights.mobDebuff ?? 0; settingsContainer.querySelector('#characterWeight').value = this.tomeSettings.weights.character ?? 0; settingsContainer.querySelector('#incomePerSpaceThreshold').value = this.tomeSettings.weights.incomePerSpaceThreshold ?? 0; settingsContainer.querySelector('#wbCharacterWeight').value = this.tomeSettings.weights.wbCharacter ?? 0; settingsContainer.querySelector('#wbElementalConvWeight').value = this.tomeSettings.weights.wbElementalConv ?? 0; settingsContainer.querySelector('#wbPowerPerSpaceThreshold').value = this.tomeSettings.weights.wbPowerPerSpaceThreshold ?? 0; settingsContainer.querySelector('#hideAddedMobs').checked = this.tomeSettings.hideMods.addedMobs ?? false; settingsContainer.querySelector('#hideReward').checked = this.tomeSettings.hideMods.reward ?? false; settingsContainer.querySelector('#hideMobDebuff').checked = this.tomeSettings.hideMods.mobDebuff ?? false; settingsContainer.querySelector('#hideCharacter').checked = this.tomeSettings.hideMods.character ?? false; settingsContainer.querySelector('#hideWaterResistance').checked = this.tomeSettings.hideMods.waterResistance ?? false; settingsContainer.querySelector('#hideThunderResistance').checked = this.tomeSettings.hideMods.thunderResistance ?? false; settingsContainer.querySelector('#hideFireResistance').checked = this.tomeSettings.hideMods.fireResistance ?? false; settingsContainer.querySelector('#hideMeleeResistance').checked = this.tomeSettings.hideMods.meleeResistance ?? false; settingsContainer.querySelector('#hideRangedResistance').checked = this.tomeSettings.hideMods.rangedResistance ?? false; settingsContainer.querySelector('#hideElementalConversion').checked = this.tomeSettings.hideMods.elementalConversion ?? false; settingsContainer.querySelector('#hideFortifyReduction').checked = this.tomeSettings.hideMods.fortifyReduction ?? false; settingsContainer.querySelector('#goldPerKillForTomesEquipped').value = this.tomeSettings.goldKillTomesEquippedAmount ?? 0; settingsContainer.querySelector('#disableRefreshOnHighlight').checked = this.tomeSettings.disableRefreshOnHighlight ?? true; if (this.tomeSettings.useWeightSettings) { settingsContainer.querySelector('#toggleWeightSettings').className = 'mat-focus-indicator mat-stroked-button mat-button-base'; } else { settingsContainer.querySelector('#toggleThresholdSettings').className = 'mat-focus-indicator mat-stroked-button mat-button-base'; } // Set up buttons openTomeSettingsbutton.onclick = () => { // Toggle open and close menu const container = document.querySelector('#tomeSettingsContainer'); if (container.style.display === 'none') { container.style.display = 'inline-block'; } else { container.style.display = 'none'; } }; const toggleWeightSettings = (yes) => { const thresholdSettingsBlock = settingsContainer.querySelector('#thresholdSettingsBlock'); const weightSettingsBlock = settingsContainer.querySelector('#tomeWeightSettingsBlock'); const toggleThresholdButton = settingsContainer.querySelector('#toggleThresholdSettings'); const toggleWeightButton = settingsContainer.querySelector('#toggleWeightSettings'); if(yes) { weightSettingsBlock.style.display = 'block'; thresholdSettingsBlock.style.display = 'none'; this.tomeSettings.useWeightSettings = true; toggleThresholdButton.className = 'mat-focus-indicator mat-raised-button mat-button-base'; toggleWeightButton.className = 'mat-focus-indicator mat-stroked-button mat-button-base'; } else { weightSettingsBlock.style.display = 'none'; thresholdSettingsBlock.style.display = 'block'; this.tomeSettings.useWeightSettings = false; toggleThresholdButton.className = 'mat-focus-indicator mat-stroked-button mat-button-base'; toggleWeightButton.className = 'mat-focus-indicator mat-raised-button mat-button-base'; } } settingsContainer.querySelector('#toggleThresholdSettings').onclick = () => toggleWeightSettings(false); settingsContainer.querySelector('#toggleWeightSettings').onclick = () => toggleWeightSettings(true); settingsContainer.querySelector('#tomeSettingsSaveButton').onclick = () => { // Get all of the values const container = document.querySelector('#tomeSettingsContainer'); const tomeSettings = { thresholds: { reward: container.querySelector('#rewardHighlightSetting').valueAsNumber * 100, mobDebuff: container.querySelector('#mobHighlightSetting').valueAsNumber * 100, character: container.querySelector('#characterHighlightSetting').valueAsNumber * 100, characterWb: container.querySelector('#characterWbHighlightSetting').valueAsNumber * 100, elementalConv: container.querySelector('#elementalConvHighlightSetting').valueAsNumber * 100, multiMob: container.querySelector('#multiMobHighlightSetting').valueAsNumber * 100, lifesteal: container.querySelector('#lifestealHighlightSetting').valueAsNumber * 100, actionSpeed: container.querySelector('#actionSpeedHighlightSetting').valueAsNumber * 100, mobSkip: container.querySelector('#mobSkipHighlightSetting').valueAsNumber * 100, numGoodRolls: container.querySelector('#numGoodRolls').valueAsNumber, numGoodRollsWb: container.querySelector('#numGoodRollsWb').valueAsNumber, }, spaceThresholds: { reward: container.querySelector('#rewardSpaceSetting').valueAsNumber, mobDebuff: container.querySelector('#mobSpaceSetting').valueAsNumber, character: container.querySelector('#characterSpaceSetting').valueAsNumber, wb: container.querySelector('#wbSpaceSetting').valueAsNumber, rare: container.querySelector('#rareSpaceSetting').valueAsNumber, legendary: container.querySelector('#legendarySpaceSetting').valueAsNumber, }, weights: { actionSpeed: container.querySelector('#actionSpeedWeight').valueAsNumber, mobSkip: container.querySelector('#mobSkipWeight').valueAsNumber, multiMob: container.querySelector('#multiMobWeight').valueAsNumber, lifesteal: container.querySelector('#lifestealWeight').valueAsNumber, reward: container.querySelector('#rewardWeight').valueAsNumber, mobDebuff: container.querySelector('#mobDebuffWeight').valueAsNumber, character: container.querySelector('#characterWeight').valueAsNumber, incomePerSpaceThreshold: container.querySelector('#incomePerSpaceThreshold').valueAsNumber, wbCharacter: container.querySelector('#wbCharacterWeight').valueAsNumber, wbElementalConv: container.querySelector('#wbElementalConvWeight').valueAsNumber, wbPowerPerSpaceThreshold: container.querySelector('#wbPowerPerSpaceThreshold').valueAsNumber, }, hideMods: { addedMobs: container.querySelector('#hideAddedMobs').checked, reward: settingsContainer.querySelector('#hideReward').checked, mobDebuff: settingsContainer.querySelector('#hideMobDebuff').checked, character: settingsContainer.querySelector('#hideCharacter').checked, waterResistance: settingsContainer.querySelector('#hideWaterResistance').checked, thunderResistance: settingsContainer.querySelector('#hideThunderResistance').checked, fireResistance: settingsContainer.querySelector('#hideFireResistance').checked, meleeResistance: settingsContainer.querySelector('#hideMeleeResistance').checked, rangedResistance: settingsContainer.querySelector('#hideRangedResistance').checked, elementalConversion: settingsContainer.querySelector('#hideElementalConversion').checked, fortifyReduction: settingsContainer.querySelector('#hideFortifyReduction').checked, }, goldKillTomesEquippedAmount: container.querySelector('#goldPerKillForTomesEquipped').valueAsNumber, disableRefreshOnHighlight: container.querySelector('#disableRefreshOnHighlight').checked, }; // Sanitize inputs for (const type in tomeSettings) { if (typeof tomeSettings[type] === 'object') { for (const [key, value] of Object.entries(tomeSettings[type])) { this.tomeSettings[type][key] = isNaN(value) ? this.tomeSettings[type][key] : value; } } else { this.tomeSettings[type] = isNaN(tomeSettings[type]) ? this.tomeSettings[type] : tomeSettings[type]; } } localStorage.setItem(`${this.playerId}:QuesBS_tomeSettings`, JSON.stringify(this.tomeSettings)); this.gameData.snackbarService.openSnackbar('Store settings saved successfully'); // Refresh highlighting const target = $('app-catacomb-tome-store > .scrollbar > div > div > .d-flex.flex-wrap.gap-1'); this.handleCatacombTomeStore({target: target[0]}); } } async insertFuseFrenzyButton() { // Add a fuse button to the gem page // Wait for page to load and get the html nodes required let gemInvOverview = document.querySelector('app-inventory-gems'); while(!gemInvOverview) { await new Promise(resolve => setTimeout(resolve, 50)); gemInvOverview = document.querySelector('app-inventory-gems'); } let gemInvTopBar = gemInvOverview.firstChild.firstChild.firstChild; // Create button const buttonContainer = document.createElement('div'); const fuseFrenzyButton = document.createElement('div'); fuseFrenzyButton.id = 'fuseFrenzyButton'; fuseFrenzyButton.className = 'mat-focus-indicator mat-raised-button mat-button-base'; fuseFrenzyButton.innerText = 'Fuse Frenzy'; // Add button buttonContainer.appendChild(fuseFrenzyButton); gemInvTopBar.appendChild(buttonContainer); fuseFrenzyButton.onclick = async () => { await this.fuseFrenzy(); }; } async fuseFrenzy() { // If this.gems has no gems, get gems from code if (this.gems.length < 1) { // Sort by level (descending) and filter frenzy gems out this.gems = this.gameData.playerInventoryService.gems.filter( gem => gem.gem_type != 'frenzy' && gem.on_market === 0 && gem.trashed === 0 ).toSorted( (gemA, gemB) => gemB.gem_level - gemA.gem_level ); } if (this.gems.length < 3) { // No valid gems to fuse, return early this.gameData.snackbarService.openSnackbar(`There aren't enough gems to fuse!`); return; } // take the ids of the last 3 gems (lowest level gems) and fuse const lowestLevelGems = [this.gems.pop(), this.gems.pop(), this.gems.pop()]; const frenzyLevel = Math.round((lowestLevelGems[0].gem_level + lowestLevelGems[1].gem_level + lowestLevelGems[2].gem_level) / 3); // Disable fuse button until gem inventory has been updated const fuseButton = document.querySelector('#fuseFrenzyButton'); fuseButton.disabled = true; fuseButton.className = 'mat-focus-indicator mat-stroked-button mat-button-base'; this.gameData.httpClient.post('/inventory/fuse-frenzy-gem', {gemIds: [lowestLevelGems[0].id, lowestLevelGems[1].id, lowestLevelGems[2].id]}).subscribe( async val => { this.gameData.snackbarService.openSnackbar(`A level ${frenzyLevel} frenzy gem was created.`); //feedback popup fuseButton.disabled = false; fuseButton.className = 'mat-focus-indicator mat-raised-button mat-button-base'; }, response => { this.gameData.snackbarService.openSnackbar(`The gem failed to be created. (Gems may be out of sync with the server)`); console.log('QuestBS: Frenzy gem could not be created.', response); fuseButton.disabled = false; fuseButton.className = 'mat-focus-indicator mat-raised-button mat-button-base'; } ); } } // ---------------------------------------------------------------------------- // Helper functions function getCatacombEndTime(numMobs, actionTimerSeconds, extraSeconds=0) { const current = new Date(); const options = { month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hourCycle: 'h23', }; const finishTime = new Date(current.getTime() + (numMobs * actionTimerSeconds + extraSeconds) * 1000) .toLocaleString('en-US', options); return finishTime; } function getStatRatios(statBlockElem) { /* Given an element statBlockElem containing rows of the 4 stats displayed at the top of the page, return the ratios between the stats */ const stats = []; for (let i = 0; i < statBlockElem.children.length; i++) { const row = statBlockElem.children[i]; stats.push(parseNumber(row.children[1].firstChild.innerText)); } const minStat = Math.min(...stats); return [ (stats[0] / minStat).toFixed(2), (stats[1] / minStat).toFixed(2), (stats[2] / minStat).toFixed(2), (stats[3] / minStat).toFixed(2), ]; } function parseNumber(num) { /** * Given a num (string), detect the type of number formatting it uses and then * convert it to the type Number. **/ // First strip any commas const resultNumStr = num.replace(/,/g, ''); if (!isNaN(Number(resultNumStr))) { // This can also convert exponential notation return Number(resultNumStr); } // Check if string has suffix const suffixes = ["k", "m", "b", "t", "qa", "qi", "sx", "sp"]; const suffixMatch = resultNumStr.match(/[a-z]+\b/g); if (suffixMatch) { const suffix = suffixMatch[0]; const shortenedNum = parseFloat(resultNumStr.match(/[0-9.]+/g)[0]); const multiplier = 1000 ** (suffixes.findIndex(e => e === suffix) + 1) if (multiplier < 1000) { console.log('QuesBS: ERROR, number\'s suffix not found in existing list'); return 0; } else { return shortenedNum * multiplier; } } } function addInvisibleScrollDiv() { /** * Add an invisible div to stop the window from scrolling via the spacebar */ const invisiDiv = document.createElement('button'); invisiDiv.id = 'stopScrollDiv'; invisiDiv.onclick = () => {}; invisiDiv.style.width = '0px'; invisiDiv.style.height = '0px'; invisiDiv.style.opacity = 0; document.body.appendChild(invisiDiv); } // ---------------------------------------------------------------------------- // This is where the script starts var QuesBS = null; console.log('QuesBS: Init load'); let QuesBSLoader = null; let numAttempts = 30; QuesBSLoader = setInterval(setupScript, 3000); window.startQuesBS = () => { // If script doesn't start, call this function (ie. startQuesBS() in the console) QuesBSLoader = setInterval(setupScript, 3000); } window.restartQuesBS = () => { // Try to reload the game data for the script QuesBSLoader = setInterval(async () => { if (QuesBS.gameData === undefined) { await QuesBS.getGameData(); } else { clearInterval(QuesBSLoader); console.log('QuesBS: Script has been reloaded.') } }, 3000); } async function setupScript() { if(QuesBS === null) { QuesBS = new Script(); await QuesBS?.getGameData(); } if(QuesBS !== null && QuesBS.gameData !== undefined) { console.log('QuesBS: The script has been loaded.'); clearInterval(QuesBSLoader); await QuesBS.initPathDetection(); await QuesBS.initPlayerData(); } else { await QuesBS?.getGameData(); console.log('QuesBS: Loading failed. Trying again...'); numAttempts--; if(numAttempts <= 0) { clearInterval(QuesBSLoader); //Stop trying after a while console.log('QuesBS: Loading failed. Stopping...'); } } }