您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays calculated values for XP, Dust, Shards, and Quest progress in Manarion game
// ==UserScript== // @name Manarion // @namespace http://tampermonkey.net/ // @version 2025-05-30 // @description Displays calculated values for XP, Dust, Shards, and Quest progress in Manarion game // @license MIT // @author Rook // @match *://manarion.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=manarion.com // @grant none // ==/UserScript== /* ======= Changelog ======= v2025-05-30 Reworked many things to read from game instead of scraping HTML, patterned after Elnaeth's work: https://greasyfork.org/en/scripts/535505-stats-shards-xp-dust-quest-res-loot-and-level-tracker Will continue to improve this script, but it is now functional again and should avoid breaking with further UI updates. */ "use strict"; (function() { //'use strict'; const debug = false; const actionsPerMin = 20;//Every 3 seconds const timeFormatter = new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }); let grid = document.querySelector('#root > div > div.flex > div.border-primary > div.grid'); // 1. Define ItemTypes for easy reference const ItemTypes = Object.freeze({ MANA_DUST: { id: 1, name: "Mana Dust", rarity: "common" }, ELEMENTAL_SHARDS: { id: 2, name: "Elemental Shards", rarity: "common" }, CODEX: { id: 3, name: "Codex", rarity: "epic" }, FIRE_ESSENCE: { id: 4, name: "Fire Essence", rarity: "rare" }, WATER_ESSENCE: { id: 5, name: "Water Essence", rarity: "rare" }, NATURE_ESSENCE: { id: 6, name: "Nature Essence", rarity: "rare" }, FISH: { id: 7, name: "Fish", rarity: "common" }, WOOD: { id: 8, name: "Wood", rarity: "common" }, IRON: { id: 9, name: "Iron", rarity: "common" }, ASBESTOS: { id: 10, name: "Asbestos", rarity: "uncommon" }, IRONBARK: { id: 11, name: "Ironbark", rarity: "uncommon" }, FISH_SCALES: { id: 12, name: "Fish Scales", rarity: "uncommon" }, TOME_OF_FIRE: { id: 13, name: "Tome of Fire", rarity: "uncommon" }, TOME_OF_WATER: { id: 14, name: "Tome of Water", rarity: "uncommon" }, TOME_OF_NATURE: { id: 15, name: "Tome of Nature", rarity: "uncommon" }, TOME_OF_MANA_SHIELD: { id: 16, name: "Tome of Mana Shield", rarity: "epic" }, ENCHANT_FIRE_RESISTANCE: { id: 17, name: "Formula: Fire Resistance", rarity: "epic" }, ENCHANT_WATER_RESISTANCE: { id: 18, name: "Formula: Water Resistance", rarity: "epic" }, ENCHANT_NATURE_RESISTANCE: { id: 19, name: "Formula: Nature Resistance", rarity: "epic" }, ENCHANT_INFERNO: { id: 20, name: "Formula: Inferno", rarity: "epic" }, ENCHANT_TIDAL_WRATH: { id: 21, name: "Formula: Tidal Wrath", rarity: "epic" }, ENCHANT_WILDHEART: { id: 22, name: "Formula: Wildheart", rarity: "epic" }, ENCHANT_INSIGHT: { id: 23, name: "Formula: Insight", rarity: "epic" }, ENCHANT_BOUNTIFUL_HARVEST: { id: 24, name: "Formula: Bountiful Harvest", rarity: "epic" }, ENCHANT_PROSPERITY: { id: 25, name: "Formula: Prosperity", rarity: "epic" }, ENCHANT_FORTUNE: { id: 26, name: "Formula: Fortune", rarity: "epic" }, ENCHANT_GROWTH: { id: 27, name: "Formula: Growth", rarity: "epic" }, ENCHANT_VITALITY: { id: 28, name: "Formula: Vitality", rarity: "epic" }, REAGENT_ELDERWOOD: { id: 29, name: "Elderwood", rarity: "uncommon" }, REAGENT_LODESTONE: { id: 30, name: "Lodestone", rarity: "uncommon" }, REAGENT_WHITE_PEARL: { id: 31, name: "White Pearl", rarity: "uncommon" }, REAGENT_FOUR_LEAF_CLOVER: { id: 32, name: "Four Leaf Clover", rarity: "uncommon" }, REAGENT_ENCHANTED_DROPLET: { id: 33, name: "Enchanted Droplet", rarity: "uncommon" }, REAGENT_INFERNAL_HEART: { id: 34, name: "Infernal Heart", rarity: "uncommon" }, ORB_OF_POWER: { id: 35, name: "Orb of Power", rarity: "rare" }, ORB_OF_CHAOS: { id: 36, name: "Orb of Chaos", rarity: "epic" }, ORB_OF_DIVINITY: { id: 37, name: "Orb of Divinity", rarity: "legendary" }, SUNPETAL: { id: 39, name: "Sunpetal", rarity: "rare" }, SAGEROOT: { id: 40, name: "Sageroot", rarity: "common" }, BLOOMWELL: { id: 41, name: "Bloomwell", rarity: "common" }, }); //let manaDustGain = 0; let manaDustForSpellpower = 0; let manaDustForWard = 0; const STORAGE_KEY = 'manarionShardTrackerData'; const MAX_LOOT_ENTRIES = 100; // Load from localStorage let stored = localStorage.getItem(STORAGE_KEY); let shardDrops = stored ? JSON.parse(stored) : []; let seenEntries = new Set(shardDrops.map(e => `${e.timestamp}|${e.shardAmount}`)); let lootDropCount = 0; //let xpGain, lastXP = 0; let levelsPerHour = 0; let minsToLevel = 0; let questActionsRemaining = -1; // 2. Centralized gain tracking let lastXP = 0, lastDust = 0, lastResource = 0; let xpGain = 0, manaDustGain = 0, resourceGain = 0; addEventListener("load", main) setTimeout(main, 5000) function main() { if (!manarion || !manarion.player) return false; grid = document.querySelector('#root > div > div.flex > div.border-primary > div.grid'); if (isBattling()) trackLastBattleGains(); if (isGathering()) trackLastGatheringGains(); perHour("XP",xpGain); perHour("Levels",levelsPerHour, "", false); perHour("Dust",manaDustGain); perHour("Shards", shardsPerHour(), "", false); etaUntil("Level", Math.round(minsToLevel)); // 4. Main tick/update loop setInterval(mainTick, 3000); } // keep track of what kind of thing we're doing right now const isBattling = () => manarion.player.ActionType === "battle"; const isGathering = () => ["mining", "fishing", "woodcutting"].includes(manarion.player.ActionType); const trackLastBattleGains = () => { lastXP = 0; lastDust = 0; lastResource = 0; if (!manarion.battle) return; const lastBattle = manarion.battle; xpGain = lastXP = lastBattle.ExperienceGained ? parseInt(lastBattle.ExperienceGained) : 0; manaDustGain = lastDust = lastBattle.Loot ? parseInt(lastBattle.Loot[ItemTypes.MANA_DUST.id]) : 0; }; const trackLastGatheringGains = () => { lastXP = 0; lastResource = 0; lastDust = 0; if (!manarion.gather) return; const lastGather = manarion.gather; xpGain = lastXP = lastGather.ExperienceGained ? parseFloat(lastGather.ExperienceGained) : 0; switch (manarion.player.ActionType) { case "mining": lastResource = lastGather.Loot ? parseFloat(lastGather.Loot[ItemTypes.IRON.id]) : 0; break; case "fishing": lastResource = lastGather.Loot ? parseFloat(lastGather.Loot[ItemTypes.FISH.id]) : 0; break; case "woodcutting": lastResource = lastGather.Loot ? parseFloat(lastGather.Loot[ItemTypes.WOOD.id]) : 0; break; } }; function timeToQuest(){ /*const questElement = document.querySelector("#root > div > div.flex.max-w-screen > div.border-primary.w-full > div.grid.grid-cols-4 > div:nth-child(2) > div > p") //document.querySelector("#root > div > div.flex > div.border-primary > div.grid.grid-cols-4 > div:nth-child(1) > div > p") const questText = questElement.textContent;//"Defeat 21/333 enemies." const questNums = questText.match(/\d+/g); const questProgress = questNums[0]; const questGoal = questNums[1];*/ questActionsRemaining = getQuestActionsRemaining(); const minsToQuest = Math.round(questActionsRemaining / actionsPerMin, 1); etaUntil("Quest", minsToQuest); } function shardsPerHour() { if (shardDrops.length < 2) { return 0; // Not enough data to calculate rate } const times = shardDrops.map(d => new Date(d.timestamp)); const firstTime = times[0]; const lastTime = times[times.length - 1]; const totalTimeMs = lastTime - firstTime; if (totalTimeMs <= 0) return 0; const totalShards = shardDrops.reduce((sum, drop) => sum + drop.shardAmount, 0); const avgShards = totalShards / shardDrops.length; const totalTimeHours = totalTimeMs / (1000 * 60 * 60); const shardsPerHour = totalShards / totalTimeHours; if(debug){ console.log(`Total Shards: ${totalShards}`); console.log(`Average Shards per Drop: ${avgShards.toFixed(2)}`); console.log(`Duration: ${totalTimeHours.toFixed(2)} hours`); console.log(`Shards per Hour: ${shardsPerHour.toFixed(2)}`); } return shardsPerHour; } function resourceGathering(){ const ul = document.querySelector("#root > div > div.flex > main > div > div.mt-4 > ul"); if(!ul){ console.log("ul = ", ul); return; } const liElements = ul.querySelectorAll('li'); let results = []; liElements.forEach(li => { const spanWithTitle = li.querySelector('span[title]'); const resourceSpan = li.querySelector('span.rarity-common'); if (!spanWithTitle || !resourceSpan) return; const title = spanWithTitle.getAttribute('title'); const gain = parseFloat(title); // Extract resource name from text content: "[Iron]" → "Iron" const resourceText = resourceSpan.textContent.trim(); const nameMatch = resourceText.match(/\[(.*?)\]/); const name = nameMatch ? nameMatch[1] : null; if (name && !isNaN(gain)) { results.push({ resourceName: name, resourceGain: gain }); } let resourceName = results[0].resourceName; let resourceGain = results[0].resourceGain; let resourceOptions = ["Iron","Fish","Wood"]; results.forEach(result => { if(resourceOptions.includes(result.resourceName)){ resourceName = result.resourceName; resourceGain = result.resourceGain; perHour(resourceName, resourceGain, "Resources"); return; } }); }); } function scanLootTracker() { const lootTrackerHeader = Array.from(document.querySelectorAll('div.relative.mb-1.text-center.text-lg')) .find(div => div.textContent.includes('Loot Tracker')); if (!lootTrackerHeader) return; const lootContainer = lootTrackerHeader.nextElementSibling; if (!lootContainer) return; const allLootRows = Array.from(lootContainer.children); const totalEntries = allLootRows.length; const lootEntries = lootContainer.querySelectorAll('div.rarity-common'); let elementalShardCount = 0; let newDataAdded = false; lootEntries.forEach(entry => { if (!entry.textContent.includes('[Elemental Shards]')) return; elementalShardCount++; const spans = entry.querySelectorAll('span[title]'); let timestamp = null; let shardAmount = null; spans.forEach(span => { const title = span.getAttribute('title'); if (/\d{1,2}:\d{2}:\d{2}/.test(span.textContent)) { timestamp = span.textContent.trim(); } else if (/[\d,]+/.test(title)) { shardAmount = parseInt(title.replace(/,/g, ''), 10); } }); if (timestamp && shardAmount !== null) { const dateObj = parseTimeString(timestamp); const iso = dateObj.toISOString(); const key = `${iso}|${shardAmount}`; if (!seenEntries.has(key)) { seenEntries.add(key); shardDrops.push({ timestamp: iso, shardAmount }); newDataAdded = true; } } }); // 🧹 Remove legacy-format entries (non-ISO timestamps) const cleaned = shardDrops.filter(entry => { const isValid = typeof entry.timestamp === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(entry.timestamp); if (!isValid) seenEntries.delete(`${entry.timestamp}|${entry.shardAmount}`); return isValid; }); if (cleaned.length !== shardDrops.length) { shardDrops.length = 0; shardDrops.push(...cleaned); newDataAdded = true; } // 🔃 Sort by timestamp (oldest first) shardDrops.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); // ✂ Trim to MAX_ENTRIES if (shardDrops.length > MAX_LOOT_ENTRIES) { const removed = shardDrops.splice(0, shardDrops.length - MAX_LOOT_ENTRIES); removed.forEach(e => seenEntries.delete(`${e.timestamp}|${e.shardAmount}`)); newDataAdded = true; } const shardRatio = (elementalShardCount / totalEntries * 100).toFixed(2); if (newDataAdded) { saveToLocalStorage(); console.log(`Shards: ${elementalShardCount}, Total: ${totalEntries}, Ratio: ${shardRatio}%`); perHour("Shards", shardsPerHour(), "", false); } } function saveToLocalStorage() { localStorage.setItem(STORAGE_KEY, JSON.stringify(shardDrops)); } function onHarvestPage(){ const harvestDiv = document.querySelector("#root > div > div.flex > main > div > div:nth-child(1)"); if(!harvestDiv){return false;} const text = harvestDiv.textContent.trim(); const regex = /You went (woodcutting|mining|fishing) and gained .* experience/i; return regex.test(text); } function timeToLevel() { //const xpGainTextElementBattle = document.querySelector("#root > div > div.flex.max-w-screen > main > div > div:nth-child(3) > p.text-green-400") //const xpGainTextElementHarvest = document.querySelector("#root > div > div.flex > main > div > div:nth-child(1) span[title]"); //let xpGainText = ""; let xp; let xpGoal; if(isBattling()){ xp = manarion.player.Experience; xpGoal = manarion.player.ExperienceToLevel; //manaDustGain = detectFloat(document.querySelector('#root > div > div.flex > main > div > div:nth-child(4) > ul > li > span:nth-child(1)').title); //xpGainText = xpGainTextElementBattle.textContent; //console.log(xpGainText); //xpGain = detectInt(xpGainText); } else if(isGathering()){ resourceGathering(); switch (manarion.player.ActionType) { case "mining": xp = manarion.player.MiningExperience; xpGoal = manarion.player.MiningExperienceToLevel; break; case "fishing": xp = manarion.player.FishingExperience; xpGoal = manarion.player.FishingExperienceToLevel; break; case "woodcutting": xp = manarion.player.WoodcuttingExperience; xpGoal = manarion.player.WoodcuttingExperienceToLevel; break; } //xpGainText = xpGainTextElementHarvest.title; //xpGain = detectFloat(xpGainText); } //const xp = parseInt(document.querySelector('#root > div > div.flex > div.border-primary > div.grid > div:nth-child(5) > span.break-all > span:nth-child(1)').title.replace(/,/g, "")); //const xpGoal = parseInt(document.querySelector('#root > div > div.flex > div.border-primary > div.grid > div:nth-child(5) > span.break-all > span:nth-child(2)').title.replace(/,/g, "")); const xpDiff = xpGoal - xp; const xpPerMinute = xpGain * actionsPerMin; const xpPerHour = xpGain * actionsPerMin * 60; if (!xpGain || xpPerMinute === 0) { console.warn("xpGain is zero or falsy, can't compute time."); minsToLevel = Infinity; levelsPerHour = 0; } else { minsToLevel = xpDiff / xpPerMinute; levelsPerHour = xpPerHour / xpGoal; } if(debug){ //console.log(xpGainText); console.log("XpGain: " + xpGain); console.log("Current XP: " + xp); console.log("XP Goal: " + xpGoal); console.log("XP Diff: " + xpDiff); console.log("MinsToLevel: " + minsToLevel); console.log("ManaDustGain: " + manaDustGain); console.log("minsToEntireLevel: " + minsToEntireLevel); console.log("levelsPerHour: " + levelsPerHour); } perHour("XP",xpGain); perHour("Levels",levelsPerHour, "", false); perHour("Dust",manaDustGain); perHour("Shards", shardsPerHour(), "", false); etaUntil("Level", Math.round(minsToLevel)); } function research(){ const onResearchPage = document.querySelector('div.space-y-5'); if(onResearchPage){ if(debug){ console.log("On the Research page"); } const manaDust = parseInt(document.querySelector("#root > div > div.flex > div.border-primary > div.grid > div:nth-child(6) > span:nth-child(2) > span").title.replace(/,/g, "")); const manaDustForSpellpowerElement = document.querySelector("div.space-y-5 > div:nth-child(1) > div:nth-child(2) > div:nth-child(2) > div:nth-child(1) > div:nth-child(4) > span:nth-child(1)"); if(manaDustForSpellpowerElement){ if(debug){ console.log("Spellpower"); } manaDustForSpellpower = parseInt(manaDustForSpellpowerElement.title.replace(/,/g, "")); manaDustForSpellpower -= manaDust; const minsToSpellpower = Math.round(manaDustForSpellpower / (actionsPerMin * manaDustGain), 1); etaUntil("Spellpower", minsToSpellpower); } const manaDustForWardElement = document.querySelector("div.space-y-5 > div:nth-child(1) > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div:nth-child(4) > span:nth-child(1)"); if(manaDustForWardElement){ if(debug){ console.log("Ward"); } manaDustForWard = parseInt(manaDustForWardElement.title.replace(/,/g, "")); manaDustForWard -= manaDust; const minsToWard = Math.round(manaDustForWard / (actionsPerMin * manaDustGain),1); etaUntil("Ward", minsToWard); } } } function updateTitle(){ if(questActionsRemaining<0){return;} let docTitle = document.title; const endIndex = indexOfEndOfWord(docTitle, "Manarion"); docTitle = trimStringAtIndex(docTitle, endIndex); if(questActionsRemaining==0){ docTitle += " | QUEST"; } else{ docTitle += " | " + questActionsRemaining; } document.title = docTitle; } function indexOfEndOfWord(text, word) { const index = text.indexOf(word); if (index === -1) { return -1; } return index + word.length; } function trimStringAtIndex(str, index) { if (index < 0 || index >= str.length) { return str; } return str.slice(0, index); } function insertString(originalString, stringToInsert, index) { if (index < 0 || index > originalString.length) { return "Index is out of bounds"; } return originalString.slice(0, index) + stringToInsert + originalString.slice(index); } function perHour(name, value, alternateId = "", doCalc = true){ if(doCalc) { value = formatNumberWithCommas(value * actionsPerMin * 60); } if(typeof value === 'number') { value = value.toFixed(2); } addGridRow(name + "/Hour", value, alternateId); } function etaUntil(event, minutes){ let eta = ""; if (!isFinite(minutes)) { eta = "Unknown"; } else if(minutes > 0){ eta = "(" + etaPhrase(minutes) + ") " + timeStamp(timePlusMinutes(minutes)); } else if (minutes == 0) { eta = "<1m"; } else{ eta = "Ready"; } addGridRow("Next " + event, eta); } function etaPhrase(minutes){ let min = minutes; let days = Math.floor(min / (60 * 24)); min = min - (days * (60 * 24)); let hours = Math.floor((min / 60)); min = min - (hours * 60); let result = ""; if (days>0) { result += days + "d:"; } if (hours>0) { result += hours + "h:"; } if (min>0) { result += min + "m"; } return result; } function timePlusMinutes(minutesToAdd) { minutesToAdd = Number(minutesToAdd); if (!isFinite(minutesToAdd)) { console.warn("minutesToAdd is not a valid finite number:", minutesToAdd); return new Date(NaN); } const now = new Date(); const msToAdd = minutesToAdd * 60 * 1000; const newTime = new Date(now.getTime() + msToAdd); return newTime; } function timeStamp(time){ try { return timeFormatter.format(time); } catch (error) { if (error instanceof RangeError) { // Handle the RangeError specifically console.error("RangeError caught for time: " + time + ":", error.message); } else { // Handle other types of errors, or re-throw if necessary console.error("An unexpected error occurred:", error); } } } function parseTimeString(t) { const [h, m, s] = t.split(':').map(Number); const now = new Date(); now.setHours(h, m, s, 0); return new Date(now); // Defensive copy } function formatNumberWithCommas(number) { return number.toLocaleString('en-US'); } function detectInt(str) { const regex = /\d+/; const match = str.replace(/,/g, "").match(regex); return match ? parseInt(match[0], 10) : null; } function detectFloat(str) { const regex = /[+-]?\d+(\.\d+)?/; // Match integers or decimals, optionally signed const match = str.replace(/,/g, "").match(regex); return match ? parseFloat(match[0]) : null; } function addGridRow(label, value, alternateId = "") { let oldDiv = document.getElementById(label); let hasAlternateId = false; let spanId = label.toLowerCase().replace(" ", "-"); let labelSpan = undefined; if(alternateId.length > 0){ hasAlternateId = true; spanId = alternateId.toLowerCase().replace(" ", "-"); oldDiv = document.getElementById(alternateId); } if(oldDiv){ //If it already exists, just update the existing content labelSpan = oldDiv.querySelector("span:nth-child(1)"); labelSpan.textContent = label; const span = document.getElementById(spanId); span.textContent = value; span.title = value; return; } const newDiv = document.createElement('div'); if(hasAlternateId){ newDiv.setAttribute('id', alternateId); } else { newDiv.setAttribute('id', label); } newDiv.classList.add("col-span-2", "flex", "justify-between"); labelSpan = document.createElement('span'); labelSpan.textContent = label; const valueSpan = document.createElement('span'); valueSpan.setAttribute('id', spanId); valueSpan.textContent = value; valueSpan.title = value; const wrapper = document.createElement('span'); wrapper.appendChild(valueSpan); newDiv.appendChild(labelSpan); newDiv.appendChild(wrapper); grid.appendChild(newDiv); } // 3. Use manarion for quest progress function getQuestActionsRemaining() { if (!manarion || !manarion.player) return -1; if (manarion.player.ActionType === "battle") { return manarion.player.BattleQuestCompleted - manarion.player.BattleQuestProgress; } else if (["mining", "fishing", "woodcutting"].includes(manarion.player.ActionType)) { return manarion.player.GatherQuestCompleted - manarion.player.GatherQuestProgress; } return -1; } // 4. Main tick/update loop function mainTick() { if (isBattling()) { trackLastBattleGains(); } else if (isGathering()) { trackLastGatheringGains(); } // XP/Hour, Dust/Hour, Resource/Hour perHour("XP", xpGain); perHour("Dust", Math.round(manaDustGain)); // Quest ETA timeToLevel(); //research(); resourceGathering(); //scanLootTracker(); timeToQuest(); } setInterval(mainTick, 3000); })();