Manarion

Display calculated values

目前為 2025-05-02 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Manarion
// @namespace    http://tampermonkey.net/
// @version      2025-05-02
// @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
    });

    const 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;

    perHour("XP",xpGain);
    perHour("Levels",levelsPerHour, "", false);
    perHour("Dust",manaDustGain);
    perHour("Shards", shardsPerHour(), "", false);
    etaUntil("Level", minsToLevel);


    addEventListener("load", main)
    setTimeout(main, 5000)

    function main() {
        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(true){
            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+/;
        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);
    }
    
})();