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.

// ==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();
    }
})();