Elethor Recyclobot Calculator

Calculate platinum production and gold profit with a UI, now with searchable item selection

目前為 2024-11-09 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Elethor Recyclobot Calculator
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Calculate platinum production and gold profit with a UI, now with searchable item selection
// @author       Eugene
// @match        https://elethor.com/*
// @grant        none
// @license      GPL-3.0-or-later
// ==/UserScript==
/*
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
(function() {
    'use strict';

    // Error logging function
    function logError(message, error) {
        console.error(`Elethor Recyclobot Calculator Error: ${message}`, error);
    }

    // Global error handler
    window.addEventListener('error', function(event) {
        logError('Uncaught error', event.error);
    });

    // Define the target URL for the calculator to display
    const TARGET_URL = 'https://elethor.com/character/companion/recyclobot';

    // Define the items and their base platinum points
    const itemList = [
        { name: "Rat Pelt", points: 55 },
        { name: "Lava Worm Tooth", points: 60 },
        { name: "Tormented Scale", points: 65 },
        { name: "Skrivet Pelt", points: 70 },
        { name: "Rippling Scale", points: 75 },
        { name: "Worg Tooth", points: 80 },
        { name: "Razen Hide", points: 85 },
        { name: "T0 Scrap", points: 600 },
        { name: "T1 Scrap", points: 1000 },
        { name: "T2 Scrap", points: 2000 },
        { name: "T3 Scrap", points: 3000 },
        { name: "Puncture 1 Essence", points: 320000 },
        { name: "Puncture 2 Essence", points: 480000 },
        { name: "Puncture 3 Essence", points: 800000 },
        { name: "Puncture 4 Essence", points: 1280000 },
        { name: "Worg Claw", points: 160 },
        { name: "Azure Tusk", points: 90 },
        { name: "Fire Scale", points: 1200 },
        { name: "Slotted Talon", points: 1500 },
        { name: "Crimson Tusk", points: 95 },
        { name: "Gory Tusk", points: 100 },
        { name: "Poisonous Barb", points: 105 },
        { name: "Karth Plate", points: 110 },
        { name: "Bat Claw", points: 160 },
        { name: "T4 Scrap", points: 4000 },
        { name: "T5 Scrap", points: 5000 },
        { name: "T6 Scrap", points: 6000 },
        { name: "T7 Scrap", points: 7000 },
        { name: "Spine Fragment", points: 165 },
        { name: "Black Ink", points: 170 },
        { name: "Dark Tusk", points: 175 },
        { name: "Dark Delve", points: 180 },
        { name: "Fireproof Tongue", points: 185 },
        { name: "Serrated Thorn", points: 190 },
        { name: "Imprinted Skull", points: 195 },
        { name: "Unhinged Jawbone", points: 200 },
        { name: "Pierced Voicebox", points: 205 },
        { name: "Broken Shovel", points: 210 },
        { name: "Pincered Talon", points: 215 },
        { name: "Translucent Scale", points: 220 },
        { name: "Scarred Fang", points: 225 },
        { name: "Crooked Leg", points: 230 },
        { name: "Venom Sample", points: 235 },
        { name: "Condensed Vapor", points: 240 },
        { name: "Fragment of Silence", points: 245 },
        { name: "Unholy Remainder", points: 290 },
        { name: "Synthetic Tar", points: 295 },
        { name: "Growg Arm", points: 300 },
        { name: "Decomposing Mask", points: 305 },
        { name: "Severed Claw", points: 310 },
        { name: "Void Fragment", points: 315 },
        { name: "Shattered Larynx", points: 340 },
        { name: "Slimy Tentacle", points: 345 },
        { name: "Broken Shackle", points: 350 },
        { name: "Drop of Aether", points: 650 },
        { name: "Torn Shadow", points: 355 },
        { name: "Cursed Bane", points: 360 },
        { name: "Volatile Dust", points: 365 },
        { name: "Unsorted Valuables", points: 390 },
        { name: "Spyglass", points: 445 },
        { name: "Shattered Shield", points: 455 },
        { name: "Shimmering Spearhead", points: 465 },
        { name: "T'Chek Incisor", points: 475 },
        { name: "Cracked Chestpiece", points: 485 },
        { name: "Fractured Rocket", points: 500 },
        { name: "Curved Blade", points: 1080 }
    ];

    // Create a button to open the calculator
    let button;
    try {
        button = document.createElement('button');
        button.innerText = 'Open Recyclobot Calculator';
        button.style.padding = '10px';
        button.style.backgroundColor = '#18743c'; // Button color
        button.style.color = '#dee5ed'; // Text color
        button.style.border = 'none';
        button.style.borderRadius = '5px';
        button.style.cursor = 'pointer';
        button.style.position = 'absolute'; // Use absolute positioning for precise placement
        button.style.zIndex = '9999'; // Ensure the button appears on top
        button.style.display = 'none'; // Initially hidden
    } catch (error) {
        logError('Error creating calculator button', error);
    }

    // Function to position the button
    function positionButton() {
        try {
            const referenceElement = document.querySelector('.button.is-info.is-multiline.w-full'); // Target button
            if (referenceElement) {
                const rect = referenceElement.getBoundingClientRect();
                button.style.top = `${rect.top + window.scrollY - button.offsetHeight - 50}px`; // Move button above the reference element (50px margin)
                button.style.left = `${rect.left + window.scrollX - button.offsetWidth - 10}px`; // Position to the left with a margin
                document.body.appendChild(button); // Append button to the body
            }
        } catch (error) {
            logError('Error positioning calculator button', error);
        }
    }

    // Create a MutationObserver to detect changes in the DOM
    let observer;
    try {
        observer = new MutationObserver((mutations) => {
            // Check if the target element is in the DOM
            const referenceElement = document.querySelector('.button.is-info.is-multiline.w-full');
            if (referenceElement) {
                positionButton(); // Position the button
                observer.disconnect(); // Stop observing once the button is placed
            }
        });

        // Start observing the document body for changes
        observer.observe(document.body, { childList: true, subtree: true });
    } catch (error) {
        logError('Error setting up MutationObserver', error);
    }

    // Create the calculator UI
    let calculatorContainer;
    try {
        calculatorContainer = document.createElement('div');
        calculatorContainer.style.display = 'none'; // Initially hidden
        calculatorContainer.style.position = 'fixed';
        calculatorContainer.style.border = '2px solid #505c6c'; // Border around calculator
        calculatorContainer.style.borderRadius = '10px';
        calculatorContainer.style.backgroundColor = '#202c3c'; // Background color
        calculatorContainer.style.zIndex = '1001';
        calculatorContainer.style.padding = '20px';
    } catch (error) {
        logError('Error creating calculator container', error);
    }

    // Function to position the calculator below the "Prospector" element
    function positionCalculator() {
        try {
            const prospectorElement = document.querySelector('a[href="/character/companion/prospector"]');
            if (prospectorElement) {
                const rect = prospectorElement.getBoundingClientRect();
                calculatorContainer.style.top = `${rect.bottom + window.scrollY - 230 + 10}px`; // 250px up from the bottom of the Prospector element + 10px margin
                calculatorContainer.style.left = `${rect.left + window.scrollX}px`; // Align horizontally with the Prospector element
            }
        } catch (error) {
            logError('Error positioning calculator', error);
        }
    }

    // Set the initial position of the calculator
    try {
        positionCalculator();
        document.body.appendChild(calculatorContainer);
    } catch (error) {
        logError('Error setting initial calculator position', error);
    }

    // Add form elements to the calculator
    calculatorContainer.innerHTML = `
        <style>
            .tooltip {
                position: relative;
                display: inline-block;
                cursor: pointer;
                margin-left: 5px;
            }
            .tooltip .tooltiptext {
                visibility: hidden;
                width: 200px;
                background-color: #555;
                color: #fff;
                text-align: center;
                border-radius: 6px;
                padding: 5px;
                position: absolute;
                z-index: 1;
                bottom: 125%;
                left: 50%;
                margin-left: -100px;
                opacity: 0;
                transition: opacity 0.3s;
            }
            .tooltip:hover .tooltiptext {
                visibility: visible;
                opacity: 1;
            }
            #itemDropdown {
                background-color: #fff;
                border: 1px solid #dee5ed;
                border-radius: 5px;
                max-height: 200px;
                overflow-y: auto;
                position: absolute;
                width: 200px;
                z-index: 1000;
            }
            #itemDropdown div {
                padding: 5px;
                cursor: pointer;
                color: black; /* This matches the color of other input fields */
            }
            #itemDropdown div:hover {
                background-color: #f0f0f0;
            }
        </style>
        <h3 style="color: #dee5ed;">Elethor Recyclobot Calculator</h3>
        <p style="color: #dee5ed; font-size: 0.9em;">Made by <a href="https://elethor.com/profile/49979" target="_blank" style="color: #6cb4e4; text-decoration: underline;">Eugene</a></p>
        <div style="margin-bottom: 5px; position: relative;">
            <label style="color: #dee5ed;">Select Item:</label>
            <input type="text" id="itemSearch" placeholder="Search items..." style="border: 1px solid #dee5ed; border-radius: 5px; padding: 5px; width: 200px; margin-right: 10px; color: black;">
            <div id="itemDropdown" style="display: none;"></div>
            <span class="tooltip">ℹ️<span class="tooltiptext">Choose the item you want to recycle</span></span>
        </div>
        <div style="margin-bottom: 5px;">
            <label style="color: #dee5ed;">Bonus Platinum Level:</label>
            <input type="number" id="bonusPlatinumLevel" required style="border: 1px solid #dee5ed; border-radius: 5px; padding: 5px; width: 100px; margin-right: 10px; color: black;">
            <span class="tooltip">ℹ️<span class="tooltiptext">Your current Bonus Platinum level</span></span>
        </div>
        <div style="margin-bottom: 5px;">
            <label style="color: #dee5ed;">Exchange Rate Level:</label>
            <input type="number" id="exchangeRateLevel" required style="border: 1px solid #dee5ed; border-radius: 5px; padding: 5px; width: 100px; margin-right: 10px; color: black;">
            <span class="tooltip">ℹ️<span class="tooltiptext">Your current Exchange Rate level</span></span>
        </div>
        <div style="margin-bottom: 5px;">
            <label style="color: #dee5ed;">Gold cost per item:</label>
            <input type="number" step="0.01" id="itemGoldCost" required style="border: 1px solid #dee5ed; border-radius: 5px; padding: 5px; width: 100px; margin-right: 10px; color: black;">
            <span class="tooltip">ℹ️<span class="tooltiptext">The gold cost for each item you're recycling</span></span>
        </div>
        <div style="margin-bottom: 5px;">
            <label style="color: #dee5ed;">Gold value per platinum (in millions):</label>
            <input type="number" step="0.01" id="goldPerPlatinum" required style="border: 1px solid #dee5ed; border-radius: 5px; padding: 5px; width: 100px; margin-right: 10px; color: black;">
            <span class="tooltip">ℹ️<span class="tooltiptext">The current gold value of one platinum in millions</span></span>
        </div>

        <div style="display: flex; gap: 10px; margin-top: 10px;">
            <button id="calculateBtn" style="background-color: #18743c; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer;">Calculate</button>
            <button id="resetBtn" style="background-color: #2596be; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer;">🔄 Reset</button>
            <button id="closeBtn" style="background-color: #a02424; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer;">X Close</button>
        </div>
        <div style="display: flex; gap: 10px; margin-top: 10px;">
            <button id="copyInputBtn" style="background-color: #ea951f; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer;">📋 Copy Input</button>
            <button id="copyOutputBtn" style="background-color: #ea951f; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer;">📋 Copy Output</button>
        </div>
        <div id="results" style="margin-top: 20px;"></div>
    `;

    // Show/hide calculator on button click
    button.addEventListener('click', () => {
        try {
            // Toggle the visibility of the calculator
            if (calculatorContainer.style.display === 'none') {
                calculatorContainer.style.display = 'block';
                positionCalculator(); // Ensure it is positioned correctly when opened
                loadInputs(); // Load saved inputs when opening
            } else {
                calculatorContainer.style.display = 'none';
            }
        } catch (error) {
            logError('Error toggling calculator visibility', error);
        }
    });

    // Close button
    calculatorContainer.querySelector('#closeBtn').addEventListener('click', () => {
        try {
            calculatorContainer.style.display = 'none';
        } catch (error) {
            logError('Error closing calculator', error);
        }
    });

    // Reset button functionality
    calculatorContainer.querySelector('#resetBtn').addEventListener('click', () => {
        try {
            document.querySelector('#itemSearch').value = ''; // Clear search input
            document.querySelector('#bonusPlatinumLevel').value = '';
            document.querySelector('#exchangeRateLevel').value = '';
            document.querySelector('#itemGoldCost').value = '';
            document.querySelector('#goldPerPlatinum').value = '';
            clearInputs(); // Clear localStorage
            document.getElementById('results').innerHTML = ''; // Clear results
        } catch (error) {
            logError('Error resetting calculator', error);
        }
    });

   // Function to flash the button when clicked
    function flashButton(button) {
        try {
            const originalColor = button.style.backgroundColor;
            button.style.backgroundColor = '#4CAF50'; // Success color (green)
            setTimeout(() => {
                button.style.backgroundColor = originalColor; // Revert back after 300ms
            }, 300);
        } catch (error) {
            logError('Error flashing button', error);
        }
    }

    // Copy Output button functionality with flash effect
    calculatorContainer.querySelector('#copyOutputBtn').addEventListener('click', () => {
        try {
            const resultsText = document.getElementById('results').innerText;
            navigator.clipboard.writeText(resultsText)
            .then(() => {
                flashButton(calculatorContainer.querySelector('#copyOutputBtn')); // Flash button
                console.log('Output copied to clipboard!');
            })
            .catch(err => logError('Failed to copy output', err));
        } catch (error) {
            logError('Error copying output', error);
        }
    });

    // Copy Input button functionality with flash effect
    calculatorContainer.querySelector('#copyInputBtn').addEventListener('click', () => {
        try {
            const selectedItem = document.querySelector('#itemSearch').value;
            const bonusPlatinumLevel = document.querySelector('#bonusPlatinumLevel').value;
            const exchangeRateLevel = document.querySelector('#exchangeRateLevel').value;
            const itemGoldCost = document.querySelector('#itemGoldCost').value;
            const goldPerPlatinum = document.querySelector('#goldPerPlatinum').value;

            const inputText = `
                Selected Item: ${selectedItem}
                Bonus Platinum Level: ${bonusPlatinumLevel}
                Exchange Rate Level: ${exchangeRateLevel}
                Gold cost per item: ${itemGoldCost}
                Gold value per platinum (in millions): ${goldPerPlatinum}
            `;

            navigator.clipboard.writeText(inputText.trim())
                .then(() => {
                    flashButton(calculatorContainer.querySelector('#copyInputBtn')); // Flash button
                    console.log('Input values copied to clipboard!');
                })
                .catch(err => logError('Failed to copy input', err));
        } catch (error) {
            logError('Error copying input', error);
        }
    });

    // Searchable dropdown functionality
    const itemSearch = document.getElementById('itemSearch');
    const itemDropdown = document.getElementById('itemDropdown');
    let selectedItemPoints = 0;

    function populateDropdown(items) {
        try {
            itemDropdown.innerHTML = '';
            items.forEach(item => {
                const div = document.createElement('div');
                div.textContent = item.name;
                div.onclick = function() {
                    itemSearch.value = item.name;
                    selectedItemPoints = item.points;
                    itemDropdown.style.display = 'none';
                };
                itemDropdown.appendChild(div);
            });
            itemDropdown.style.display = 'block';
        } catch (error) {
            logError('Error populating dropdown', error);
        }
    }

    itemSearch.addEventListener('focus', function() {
        try {
            if (this.value === '') {
                populateDropdown(itemList);
            }
        } catch (error) {
            logError('Error handling item search focus', error);
        }
    });

    itemSearch.addEventListener('input', function() {
        try {
            const searchTerm = this.value.toLowerCase();
            const filteredItems = searchTerm === '' ? itemList : itemList.filter(item =>
                item.name.toLowerCase().includes(searchTerm)
            );
            populateDropdown(filteredItems);
        } catch (error) {
            logError('Error handling item search input', error);
        }
    });

    document.addEventListener('click', function(e) {
        try {
            if (e.target !== itemSearch && e.target !== itemDropdown) {
                itemDropdown.style.display = 'none';
            }
        } catch (error) {
            logError('Error handling document click', error);
        }
    });

    // Calculate button functionality
    calculatorContainer.querySelector('#calculateBtn').addEventListener('click', () => {
        try {
            const bonusPlatinumLevel = parseFloat(document.querySelector('#bonusPlatinumLevel').value);
            const exchangeRateLevel = parseFloat(document.querySelector('#exchangeRateLevel').value);
            const itemGoldCost = parseFloat(document.querySelector('#itemGoldCost').value);
            const goldPerPlatinum = parseFloat(document.querySelector('#goldPerPlatinum').value);

            // Validation: Check for positive values
            if (
                selectedItemPoints <= 0 ||
                isNaN(bonusPlatinumLevel) || bonusPlatinumLevel < 0 ||
                isNaN(exchangeRateLevel) || exchangeRateLevel < 0 ||
                isNaN(itemGoldCost) || itemGoldCost < 0 ||
                isNaN(goldPerPlatinum) || goldPerPlatinum < 0
            ) {
                throw new Error('Invalid input values');
            }

            saveInputs(selectedItemPoints, bonusPlatinumLevel, exchangeRateLevel, itemGoldCost, goldPerPlatinum);

            const ITEMS_PER_RECYCLE = 200;
            const BASE_PLATINUM_POINTS = selectedItemPoints;
            const BASE_PLATINUM_COST = 10000;
            const MAX_COST_INCREASE_PLATINUM = 20;
            const PLATINUM_GOLD_COST = 250000;
            const BONUS_PLATINUM_INCREMENT = 0.002;

            const exchangeRateMultiplier = 1 + (exchangeRateLevel * 0.01);
            const bonusPlatinumMultiplier = 1 + (bonusPlatinumLevel * BONUS_PLATINUM_INCREMENT);
            const platinumPointsPerRecycle = Math.floor(BASE_PLATINUM_POINTS * exchangeRateMultiplier);
            let totalGoldProfit = 0;
            let platinumCount = 0;
            let totalGoldSpent = 0;
            let totalItemsUsed = 0;
            let currentPlatinumCost = BASE_PLATINUM_COST;

            while (true) {
                const platinumPointsNeeded = currentPlatinumCost;
                const requiredRecycles = Math.ceil(platinumPointsNeeded / platinumPointsPerRecycle);
                const requiredItems = requiredRecycles * ITEMS_PER_RECYCLE;
                totalItemsUsed += requiredItems;

                const itemsGoldCostTotal = requiredItems * itemGoldCost;
                const totalGoldCost = PLATINUM_GOLD_COST + itemsGoldCostTotal;

                // Total platinum produced including bonus
                const totalPlatinum = 1 * bonusPlatinumMultiplier;
                const basePlatinum = 1; // Base platinum unit
                const bonusPlatinum = totalPlatinum - basePlatinum; // Bonus platinum units

                // Cost per unit of platinum
                const costPerPlatinum = totalGoldCost / totalPlatinum;

                // Calculate profit per platinum unit
                let profit = 0;

                // Profit from base platinum (100% of its gold value)
                profit += (basePlatinum * goldPerPlatinum * 1_000_000) - (costPerPlatinum * basePlatinum);

                // Profit from bonus platinum (90% of its gold value)
                profit += (bonusPlatinum * goldPerPlatinum * 1_000_000 * 0.9) - (costPerPlatinum * bonusPlatinum);

                // If profit is non-positive, break the loop
                if (profit <= 0) {
                    break;
                }

                // Accumulate the total profits and costs
                totalGoldProfit += profit;
                totalGoldSpent += totalGoldCost;
                platinumCount++;

                // Increase platinum cost for the next cycle
                if (platinumCount < MAX_COST_INCREASE_PLATINUM) {
                    currentPlatinumCost += 1000;
                } else {
                    currentPlatinumCost += 500;
                }
            }

            // Display results
            document.getElementById('results').innerHTML = `
                <p style="color: #dee5ed;">Platinum to produce: ${platinumCount}</p>
                <p style="color: #dee5ed;">Total Gold Profit: ${formatToBillions(totalGoldProfit)}</p>
                <p style="color: #dee5ed;">Maximum Bonus Platinum: ${(platinumCount * bonusPlatinumMultiplier).toFixed(2)}</p>
                <p style="color: #dee5ed;">Total Items Used: ${totalItemsUsed}</p>
            `;
        } catch (error) {
            logError('Error in calculation', error);
            alert('An error occurred during calculation. Please check your inputs and try again.');
        }
    });

    // Function to format value to billions
    function formatToBillions(value) {
        try {
            let billions = value / 1_000_000_000;
            return billions.toFixed(2) + " billion";
        } catch (error) {
            logError('Error formatting to billions', error);
            return "Error";
        }
    }

    // Save inputs to localStorage
    function saveInputs(selectedItemPoints, bonusPlatinumLevel, exchangeRateLevel, itemGoldCost, goldPerPlatinum) {
        try {
            localStorage.setItem('selectedItemPoints', selectedItemPoints);
            localStorage.setItem('bonusPlatinumLevel', bonusPlatinumLevel);
            localStorage.setItem('exchangeRateLevel', exchangeRateLevel);
            localStorage.setItem('itemGoldCost', itemGoldCost);
            localStorage.setItem('goldPerPlatinum', goldPerPlatinum);
        } catch (error) {
            logError('Error saving inputs to localStorage', error);
        }
    }

    // Load inputs from localStorage
    function loadInputs() {
        try {
            const savedItemPoints = localStorage.getItem('selectedItemPoints');
            if (savedItemPoints) {
                const savedItem = itemList.find(item => item.points == savedItemPoints);
                if (savedItem) {
                    document.querySelector('#itemSearch').value = savedItem.name;
                    selectedItemPoints = savedItem.points;
                }
            }
            document.querySelector('#bonusPlatinumLevel').value = localStorage.getItem('bonusPlatinumLevel') || '';
            document.querySelector('#exchangeRateLevel').value = localStorage.getItem('exchangeRateLevel') || '';
            document.querySelector('#itemGoldCost').value = localStorage.getItem('itemGoldCost') || '';
            document.querySelector('#goldPerPlatinum').value = localStorage.getItem('goldPerPlatinum') || '';
        } catch (error) {
            logError('Error loading inputs from localStorage', error);
        }
    }

    // Clear inputs from localStorage
    function clearInputs() {
        try {
            localStorage.removeItem('selectedItemPoints');
            localStorage.removeItem('bonusPlatinumLevel');
            localStorage.removeItem('exchangeRateLevel');
            localStorage.removeItem('itemGoldCost');
            localStorage.removeItem('goldPerPlatinum');
        } catch (error) {
            logError('Error clearing inputs from localStorage', error);
        }
    }

    // URL check function to show button and calculator only on the correct page
    function checkURL() {
        try {
            if (window.location.href === TARGET_URL) {
                button.style.display = 'block';
                if (calculatorContainer.style.display !== 'none') {
                    positionCalculator();
                }
            } else {
                button.style.display = 'none';
                calculatorContainer.style.display = 'none';
            }
        } catch (error) {
            logError('Error checking URL', error);
        }
    }

    // Check URL initially and set interval for changes
    checkURL();
    setInterval(checkURL, 500); // Check URL every 500 milliseconds

})();