Farm RPG Mining Progress Display

Displays the progress value from the mining bar, while collecting data in the background.

目前為 2025-08-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Farm RPG Mining Progress Display
// @namespace   http://tampermonkey.net/
// @version     1.3a
// @description Displays the progress value from the mining bar, while collecting data in the background.
// @author      ClientCoin
// @match       http://farmrpg.com/*
// @match       https://farmrpg.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=farmrpg.com
// @grant        GM.getValue
// @grant        GM.setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    console.log('Mining Progress Display Initiated');

    // --- CONFIGURATION ---
    const isDebugMode = false;
    const debugLog = (...args) => {
        if (isDebugMode) {
            console.log('Tampermonkey script:', ...args);
        }
    };
    // --- END CONFIGURATION ---

    // A unique key for storing data in GM_storage

    // Function to get the current mining location name from the DOM
// Function to get the current mining location name from the DOM


// ===========================================
// SECTION 1: DATA COLLECTION LOGIC
// - This section ONLY handles data storage.
// - It does NOT modify the UI.
// ===========================================
let dataRetryCount = 0;
const MAX_DATA_RETRIES = 5;

// Function to get the current mining location name from the DOM
function getMiningLocation() {
    debugLog('2. DATA: Executing getMiningLocation function...');

    const allCenterSlidingDivs = document.querySelectorAll('.center.sliding');
    debugLog(`2. DATA: - Found ${allCenterSlidingDivs.length} elements with class ".center.sliding"`);

    for (const div of allCenterSlidingDivs) {
        // Find the 'info' icon element inside the current div.
        const infoIcon = div.querySelector('a i.f7-icons');

        // This is the most reliable way to identify the mining location header.
        if (infoIcon) {
            const fullText = div.textContent;
            // The location name is the part of the string before the 'info' text.
            const locationText = fullText.substring(0, fullText.indexOf('info')).trim();
            debugLog(`2. DATA: - Found a match! Location element has 'info' icon. Location: ${locationText}`);
            return locationText;
        }
    }

    debugLog('2. DATA: - No matching element found. Returning null.');
    return null;
}

    function getCurrentFloor() {
        const floorLabel = document.querySelector('.col-30 strong');
        const floorMatch = floorLabel.textContent.match(/(\d{1,3}(?:,\d{3})*)/);
        const currentFloor = floorMatch ? parseInt(floorMatch[1].replace(/,/g, ''), 10) : null;
        debugLog("Current Floor is: " + currentFloor);
        return currentFloor;
    }

    // Function to reset all stored data for the current mining location
async function resetMiningData(locationName) {
    if (!locationName) {
        console.warn('Tampermonkey script: Cannot reset data, location name is not defined.');
        return;
    }

    // Confirmation prompt to prevent accidental data loss
    if (confirm(`Are you sure you want to delete all mining data for ${locationName}?`)) {
        try {
            let miningData = await GM_getValue(STORAGE_KEY, {});
            delete miningData[locationName];
            await GM_setValue(STORAGE_KEY, miningData);
            console.log(`Tampermonkey script: Successfully deleted all mining data for ${locationName}.`);

            // Reload the page to refresh the display
            window.location.reload();
        } catch (error) {
            console.error('Tampermonkey script: Error resetting data:', error);
        }
    }
}

// A unique key for storing data in GM_storage
const STORAGE_KEY = 'farmrpg_mining_data';

async function collectAndStoreMiningData() {
    const progressBar = document.getElementById('mpb');
    const floorElement = document.querySelector('.col-30 strong span');
    const locationName = getMiningLocation();

    if (!progressBar || !floorElement || !locationName) {
        if (dataRetryCount < MAX_DATA_RETRIES) {
            dataRetryCount++;
            setTimeout(collectAndStoreMiningData, 50);
        } else {
            dataRetryCount = 0;
        }
        return;
    }

    dataRetryCount = 0;
    const currentFloor = getCurrentFloor();
    const currentProgress = parseFloat(progressBar.getAttribute('data-progress'));

    try {
        let miningData = await GM_getValue(STORAGE_KEY, {});

        // Store the data in the new nested format: location > floor > progress
        miningData[locationName] = miningData[locationName] || {};
        miningData[locationName][currentFloor] = {
            progress: currentProgress
        };

        await GM_setValue(STORAGE_KEY, miningData);

    } catch (error) {
        console.error('Tampermonkey script: Error during data collection:', error);
    }
}
    // ===========================================
    // SECTION 2: DISPLAY LOGIC (v1.1)
    // ===========================================
// Function to get the average progress per floor from recent floors using an exponential weighted average
// Function to get the average progress per floor from recent floors using a standard deviation filter
// Function to get the average progress per floor from recent floors using a standard deviation filter
function getRecentProgressPerFloor(locationData) {
    debugLog('1. DISPLAY: Calculating progress rate...');
    const ALPHA = 0.2;
    const MAD_THRESHOLD = 2.5; // A common multiplier for the MAD to define the outlier threshold.
    const MAD_MINIMUM_PERCENTAGE_OF_MEDIAN = 0.1; // Failsafe to prevent filtering when MAD is insignificant.
    const allFloors = Object.keys(locationData).map(Number).sort((a, b) => a - b);
    const recentFloors = allFloors.slice(Math.max(0, allFloors.length - 100));

    if (recentFloors.length < 2) {
        debugLog('1. DISPLAY: Not enough data points to calculate a rate. (Need at least 2 floors)');
        return null;
    }

    let progressRates = [];
    for (let i = 1; i < recentFloors.length; i++) {
        const previousFloor = recentFloors[i - 1];
        const floorsGained = recentFloors[i] - previousFloor;

        if (floorsGained > 0) {
            const progressChange = locationData[recentFloors[i]].progress - locationData[previousFloor].progress;
            if (progressChange > 0) {
                progressRates.push(progressChange / floorsGained);
            }
        }
    }
    debugLog('1. DISPLAY: Original progress rates:', progressRates.map(rate => rate.toFixed(4)));

    if (progressRates.length < 3) {
        debugLog('1. DISPLAY: Not enough data points for statistical filtering. (Need at least 3)');
        return null;
    }

    // --- Outlier detection using MAD ---
    const sortedRates = [...progressRates].sort((a, b) => a - b);
    const median = sortedRates.length % 2 === 0
        ? (sortedRates[sortedRates.length / 2 - 1] + sortedRates[sortedRates.length / 2]) / 2
        : sortedRates[Math.floor(sortedRates.length / 2)];

    const deviations = sortedRates.map(rate => Math.abs(rate - median));
    deviations.sort((a, b) => a - b);
    let mad = deviations.length % 2 === 0
        ? (deviations[deviations.length / 2 - 1] + deviations[deviations.length / 2]) / 2
        : deviations[Math.floor(deviations.length / 2)];
    if (mad < 0.01) {mad = median * MAD_MINIMUM_PERCENTAGE_OF_MEDIAN};

    const filteredRates = progressRates.filter(rate => Math.abs(rate - median) <= MAD_THRESHOLD * mad);

    const rejectedRates = progressRates.filter(rate => !filteredRates.includes(rate));
    debugLog(`1. DISPLAY: Median: ${median.toFixed(4)}, MAD: ${mad.toFixed(4)}`);
    debugLog(`1. DISPLAY: Filtering data points within ${MAD_THRESHOLD}x MAD from median.`);
    debugLog('1. DISPLAY: Rejected rates (outliers):', rejectedRates.map(rate => rate.toFixed(4)));

    if (filteredRates.length === 0) {
        debugLog('1. DISPLAY: No valid data points remain after filtering.');
        return null;
    }

    let weightedAverage = filteredRates[0];
    for (let i = 1; i < filteredRates.length; i++) {
        weightedAverage = (ALPHA * filteredRates[i]) + ((1 - ALPHA) * weightedAverage);
    }
    debugLog(`1. DISPLAY: Final calculated weighted average: ${weightedAverage.toFixed(4)}`);
    return weightedAverage;
}


async function updateMiningDisplay() {
    const progressBar = document.getElementById('mpb');
    const floorElement = document.querySelector('.col-30 strong span');
    const locationName = getMiningLocation();

    if (!progressBar || !floorElement || !locationName) {
        return;
    }

   const floorLabel = document.querySelector('.col-30 strong');

   const currentFloor = getCurrentFloor();
   if (isNaN(currentFloor)) {
       debugLog('1. DISPLAY: Floor number is not a valid number. Exiting.');
       return;
   }

    if (floorLabel) {
        const textNode = Array.from(floorLabel.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim() === 'Floor');
        if (textNode) {
            textNode.remove();
        }
        const brElement = floorLabel.querySelector('br');
        if (brElement) {
            brElement.remove();
        }
        if (!floorLabel.querySelector('#floor-label')) {
            const labelSpan = document.createElement('span');
            labelSpan.id = 'floor-label';
            labelSpan.textContent = 'Floor: ';
            labelSpan.style.fontSize = '17px';
            labelSpan.style.fontWeight = 'bold';
            floorLabel.prepend(labelSpan);
            const floorNumberSpan = floorLabel.querySelector('span:not(#floor-label)');
            if(floorNumberSpan) floorNumberSpan.style.fontSize = '17px';
        }
    }



   const currentProgress = parseFloat(progressBar.getAttribute('data-progress'));

    let miningData = await GM_getValue(STORAGE_KEY, {});
    const locationData = miningData[locationName] || {};

    let floorsToGoText = 'Data gathering...';
    let targetFloorText = 'Complete 4 Floors to Start';
    let lastKnownProgress = 0;

    const allFloors = Object.keys(locationData).map(Number).sort((a, b) => b - a);
    if (allFloors.length > 0) {
        const lastFloorNumber = allFloors[0];
        lastKnownProgress = locationData[lastFloorNumber]?.progress || 0;
    }
    debugLog(miningData);
    if (currentProgress < lastKnownProgress) {
        miningData[locationName] = {};
        await GM_setValue(STORAGE_KEY, miningData);
    }

    const progressRate = getRecentProgressPerFloor(locationData, currentFloor);

    if (progressRate && progressRate > 0) {
        const progressRemaining = 100 - currentProgress;
        const estimatedFloorsToGo = progressRemaining / progressRate;

        floorsToGoText = `Est. Floors: ${Math.round(estimatedFloorsToGo)}`;
        targetFloorText = `Target Floor: ${Math.round(currentFloor + estimatedFloorsToGo)}`;
    }

    const createOrUpdateDisplay = (progressValue) => {
        const floorContainer = document.querySelector('.col-30');
        if (floorContainer) {
            let progressDisplay = document.getElementById('farmrpg-progress-display');
            let estimateDisplay = document.getElementById('farmrpg-estimate-display');
            let targetFloorDisplay = document.getElementById('farmrpg-target-floor-display');
            let resetButton = document.getElementById('farmrpg-reset-button');

            if (!progressDisplay) {
                progressDisplay = document.createElement('div');
                progressDisplay.id = 'farmrpg-progress-display';
                floorContainer.insertBefore(progressDisplay, floorContainer.querySelector('strong'));
                progressDisplay.style.cssText = `font-weight: bold !important; color: lightgreen !important; font-size: 14px !important; margin-bottom: 5px !important; text-shadow: 0 0 5px rgba(144, 238, 144, 0.5) !important;`;
            }
            const formattedProgress = parseFloat(progressValue).toFixed(2);
            progressDisplay.textContent = `Progress: ${formattedProgress}%`;

            if (!estimateDisplay) {
                estimateDisplay = document.createElement('div');
                estimateDisplay.id = 'farmrpg-estimate-display';
                floorContainer.insertBefore(estimateDisplay, floorContainer.querySelector('strong').nextSibling);
                estimateDisplay.style.cssText = `font-weight: bold !important; color: #add8e6 !important; font-size: 12px !important; margin-top: 5px !important;`;
            }
            estimateDisplay.textContent = floorsToGoText;

            if (!targetFloorDisplay) {
                targetFloorDisplay = document.createElement('div');
                targetFloorDisplay.id = 'farmrpg-target-floor-display';
                floorContainer.insertBefore(targetFloorDisplay, estimateDisplay.nextSibling);
                targetFloorDisplay.style.cssText = `font-weight: bold !important; color: #ffcc00 !important; font-size: 12px !important; margin-top: 5px !important;`;
            }
            targetFloorDisplay.textContent = targetFloorText;

            if (!resetButton) {
                resetButton = document.createElement('a');
                resetButton.id = 'farmrpg-reset-button';
                resetButton.className = 'button btnpurple';
                resetButton.style.cssText = `font-size: 11px; height: 20px; line-height: 18px; width: 100px; margin: 10px auto 0 auto; display: block;`;
                resetButton.textContent = 'Reset Data';
                resetButton.onclick = () => resetMiningData(locationName);
                floorContainer.insertBefore(resetButton, targetFloorDisplay.nextSibling);
            }
        }
    };

    createOrUpdateDisplay(currentProgress);
}


    // Function to remove <br> tags from the target div
function removeBreakLines() {
    const floorContainer = document.querySelector('.col-30');
    if (floorContainer) {
        const breakLines = floorContainer.querySelectorAll('br');
        //breakLines.forEach(br => br.remove());
    }
}
    // ===========================================
    // SECTION 3: ACTIVATION
    // - This section activates both functions.
    // ===========================================
    updateMiningDisplay();
    collectAndStoreMiningData();

    const targetNode = document.querySelector("#fireworks");
    if (targetNode) {
        const navObserver = new MutationObserver((mutationsList) => {
            if (mutationsList.some(m => m.attributeName === 'data-page')) {
                debugLog('3. ACTIVATION: Navigation detected via data-page attribute change.');
                updateMiningDisplay();
                collectAndStoreMiningData();
            }
        });
        navObserver.observe(targetNode, { attributes: true });
        debugLog('3. ACTIVATION: Navigation observer set up.');
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址