// ==UserScript==
// @name Manarion
// @namespace http://tampermonkey.net/
// @version 2025-05-02.4
// @description Display calculated values
// @author Rook
// @match https://manarion.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=manarion.com
// @grant none
// ==/UserScript==
(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');
let manaDustGain = 0;
let manaDustForSpellpower = 0;
let manaDustForWard = 0;
const STORAGE_KEY = 'manarionShardTrackerData';
const MAX_LOOT_ENTRIES = 1000;
// 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 = 0;
let levelsPerHour = 0;
let minsToLevel = 0;
addEventListener("load", main)
setTimeout(main, 5000)
function main() {
grid = document.querySelector('#root > div > div.flex > div.border-primary > div.grid');
perHour("XP",xpGain);
perHour("Levels",levelsPerHour, "", false);
perHour("Dust",manaDustGain);
perHour("Shards", shardsPerHour(), "", false);
etaUntil("Level", minsToLevel);
setInterval(function() {timeToLevel();}, 3000);
setInterval(function() {research();}, 3000);
setInterval(function() {scanLootTracker();}, 3000);
setInterval(function() {timeToQuest();}, 3000);
}
function timeToQuest(){
const questElement = 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];
const minsToQuest = Math.round((questGoal - questProgress) / actionsPerMin, 1);
if(minsToQuest<=0){
document.title = "Manarion QUEST";
}
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 timeToLevel() {
const xpGainTextElementBattle = document.querySelector('#root > div > div.flex > 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");
let xpGainText = "";
if(xpGainTextElementBattle){
if(debug){
console.log("On the Battle Page");
}
manaDustGain = detectFloat(document.querySelector('#root > div > div.flex > main > div > div:nth-child(4) > ul > li > span:nth-child(1)').title);
xpGainText = xpGainTextElementBattle.textContent;
xpGain = detectInt(xpGainText);
}
else if(xpGainTextElementHarvest){
if(debug){
console.log("On the Harvest Page");
}
resourceGathering();
xpGainText = xpGainTextElementHarvest.textContent;
xpGain = detectFloat(xpGainText);
}
if(xpGainTextElementBattle || xpGainTextElementHarvest){
const xp = parseInt(document.querySelector('#root > div > div.flex > div.border-primary > div.grid > div:nth-child(4) > 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(4) > span.break-all > span:nth-child(2)').title.replace(/,/g, ""));
const xpDiff = xpGoal - xp;
minsToLevel = Math.round(xpDiff / (actionsPerMin * xpGain), 1);
const minsToEntireLevel = Math.round(xpGoal / (actionsPerMin * xpGain), 1);
levelsPerHour = minsToEntireLevel / 60.0;
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", 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 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(minutes > 0){
eta = "(" + etaPhrase(minutes) + ") " + timeStamp(timePlusMinutes(minutes));
}
else if (minutes == 0)
{
eta = "<1m";
}
else{
eta = "(Loading)";
}
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) {
let now = new Date();
let currentMinutes = now.getMinutes();
let newTime = now;
newTime.setMinutes(currentMinutes + minutesToAdd);
return newTime;
}
function timeStamp(time){
return timeFormatter.format(time);
}
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);
}
})();