Farm RPG Harvest Log Sum

Extracts and permanently stores all harvested crops, calculates average drops per harvest, and outputs to the console. Includes automatic data purging.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Farm RPG Harvest Log Sum
// @namespace    ClientCoinCropSum
// @version      4.5.1
// @description  Extracts and permanently stores all harvested crops, calculates average drops per harvest, and outputs to the console. Includes automatic data purging.
// @author       ClientCoin
// @match        https://farmrpg.com/*
// @match        https://alpha.farmrpg.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=farmrpg.com
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
    'use strict';

    // --- SCRIPT SETTINGS ---
    // Set this to `true` for detailed console output, `false` to run silently.
    const DEBUG_MODE = false;

    // The maximum storage size in KB before old data is purged.
    const MAX_STORAGE_KB = 4096; // Corresponds to 4 MB

    // A map defining special sub-drops and their corresponding main crops.
    const SUB_DROPS_MAP = {
        'Gold Peppers': 'Peppers',
        'Yellow Watermelon': 'Watermelon',
        'Hot Potato': 'Potato',
        'Inferno Pepper': 'Peppers',
        'Gold Eggplant': 'Eggplant',
        'Gold Peas': 'Peas',
        'Gold Carrot': 'Carrot',
        'Gold Cucumber': 'Cucumber',
        'Mega Beet': 'Beet',
        'Mega Cotton': 'Cotton',
        'Mega Sunflower': 'Sunflower'
        // Add more special drops here if needed in the future, e.g.: 'Special Drop': 'Main Crop'
    };

    let isProcessing = false;

    /**
     * Parses the harvest log from the current page and adds any new data to persistent storage.
     * Then, it processes all the stored data to produce the final summary.
     */
    async function processHarvestLog() {
        if (isProcessing) {
            // A flag to prevent the function from running multiple times in quick succession.
            return;
        }

        isProcessing = true;

        const harvestLogTitle = document.getElementById('harvestlog');
        if (!harvestLogTitle) {
            isProcessing = false;
            return;
        }

        // 1. Correctly find the main container for the harvest log items.
        // The list of harvests is in the second `div.card` element that is a sibling
        // of the `content-block-title` element.
        const harvestLogCard = harvestLogTitle.nextElementSibling.nextElementSibling;

        if (!harvestLogCard) {
            console.error('Could not find the card container for the harvest log. Please check your page HTML.');
            isProcessing = false;
            return;
        }

        // Retrieve existing harvest data from storage.
        // Initialize an empty object if no data is found.
        const storedData = await GM_getValue('harvestData', {});
        const processedHarvests = new Set(Object.keys(storedData));

        let newHarvestsAdded = 0;
        let purgedEntries = 0;

        // 2. Iterate over the harvest log on the current page.
        harvestLogCard.querySelectorAll('.list-block > div.list-group').forEach(group => {
            const titleElement = group.querySelector('.list-group-title');
            if (!titleElement) return;

            // Use the timestamp as a unique ID for this harvest session.
            const harvestTimestamp = titleElement.textContent.trim().replace('Harvest on ', '');

            // Only process harvests we haven't seen before.
            if (!processedHarvests.has(harvestTimestamp)) {
                newHarvestsAdded++;
                processedHarvests.add(harvestTimestamp);
                storedData[harvestTimestamp] = [];

                const items = group.querySelectorAll('li.close-panel');
                const mainCropElement = items[0].querySelector('.item-title strong');
                const currentMainCropName = mainCropElement ? mainCropElement.textContent.trim() : null;

                items.forEach(item => {
                    const cropNameElement = item.querySelector('.item-title strong');
                    const harvestAmountElement = item.querySelector('.item-after');

                    const cropName = cropNameElement ? cropNameElement.textContent.trim() : null;
                    const harvestAmount = parseInt(harvestAmountElement ? harvestAmountElement.textContent.trim().replace(/,/g, '') : '0', 10);

                    if (cropName && !isNaN(harvestAmount)) {
                        storedData[harvestTimestamp].push({
                            name: cropName,
                            amount: harvestAmount,
                            isMain: cropName === currentMainCropName
                        });
                    }
                });

                // Check and purge the oldest entry if storage limit is exceeded after adding a new one.
                let storageSize = JSON.stringify(storedData).length;
                if (storageSize / 1024 > MAX_STORAGE_KB) {
                    const sortedKeys = Object.keys(storedData).sort();
                    if (sortedKeys.length > 0) {
                        delete storedData[sortedKeys[0]];
                        purgedEntries++;
                    }
                }
            }
        });

        // Save the updated data back to storage if any changes were made.
        if (newHarvestsAdded > 0 || purgedEntries > 0) {
            await GM_setValue('harvestData', storedData);
        }

        // 3. Process all stored data to generate the final summary.
        const mainCropTotals = new Map();
        const mainCropOccurrences = new Map();
        const subDropTotals = new Map();

        Object.values(storedData).forEach(sessionItems => {
            let currentMainCropName = null;
            if (sessionItems.length > 0) {
                currentMainCropName = sessionItems[0].isMain ? sessionItems[0].name : null;
            }

            if (currentMainCropName) {
                const currentMainOccurrences = mainCropOccurrences.get(currentMainCropName) || 0;
                mainCropOccurrences.set(currentMainCropName, currentMainOccurrences + 1);
            }

            sessionItems.forEach(item => {
                const isSubDrop = Object.keys(SUB_DROPS_MAP).includes(item.name);

                if (isSubDrop) {
                    const currentSubTotal = subDropTotals.get(item.name) || 0;
                    subDropTotals.set(item.name, currentSubTotal + item.amount);
                } else if (item.isMain) {
                    const currentMainTotal = mainCropTotals.get(item.name) || 0;
                    mainCropTotals.set(item.name, currentMainTotal + item.amount);
                }
            });
        });

        // Calculate storage size again after any potential purging.
        const finalStorageSize = JSON.stringify(storedData).length;
        const finalStorageSizeKB = (finalStorageSize / 1024).toFixed(2);

        // 4. Log the final summary of all crops to the console.
        console.log('--- Final Harvest Log Summary ---');
        if(DEBUG_MODE) {
            console.log(`Total Harvest Sessions Recorded: ${Object.keys(storedData).length}`);
            console.log(`Current data size: ${finalStorageSizeKB} KB`);
            if (purgedEntries > 0) {
                console.log(`Note: Automatically purged ${purgedEntries} oldest harvest entries to manage storage.`);
            }
            console.log(`To clear all stored harvest data, run 'clearHarvestData()' in the console.`);
            console.log('\nHarvest Totals and Rates:');
        }
        if (purgedEntries > 0) {
            console.log(`Note: Automatically purged ${purgedEntries} oldest harvest entries to manage storage.`);
        }

        mainCropTotals.forEach((total, crop) => {
            const occurrences = mainCropOccurrences.get(crop) || 0;
            if (occurrences > 0) {
                const aph = (total / occurrences);
                console.log(`${crop}: ${aph.toFixed(2)} aph (${total} in ${occurrences} harvests)`);
            }
        });

        subDropTotals.forEach((total, crop) => {
            const mainCrop = SUB_DROPS_MAP[crop];
            const mainCropTotal = mainCropTotals.get(mainCrop) || 0;

            if (mainCropTotal > 0 && total > 0) {
                const dropRate = (mainCropTotal / total);
                console.log(`${crop}: 1 in ${Math.round(dropRate)} (${total} from ${mainCropTotal})`);
            }
        });

        //console.log('---------------------------------');

        isProcessing = false;
    }

    // Set up the MutationObserver to listen for page changes and then run the script.
    const target = document.getElementById("fireworks");
    if (!target) {
        console.error('Target element #fireworks not found. MutationObserver cannot be set up.');
        return;
    }

    const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
            if (mutation.attributeName === "data-page") {
                const newPage = target.getAttribute("data-page");
                if (newPage === "farminfo") {
                    processHarvestLog();
                }
            }
        }
    });

    const config = {
        attributes: true,
        childList: false,
        subtree: false
    };

    observer.observe(target, config);

    // Run immediately if the user is already on the correct page when the script is loaded.
    const currentPage = target.getAttribute("data-page");
    if (currentPage === "farminfo") {
        processHarvestLog();
    }
})();