Elethor Combat Calculator

Calculate combat profit with uniform midas/extractor options.

// ==UserScript==
// @name         Elethor Combat Calculator
// @namespace    http://tampermonkey.net/
// @version      1.60
// @description  Calculate combat profit with uniform midas/extractor options.
// @author       Eugene
// @match        https://elethor.com/*
// @grant        GM_xmlhttpRequest
// @esversion   11
// @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';

    const monsterData = {
        'Apex Nassul': { mobDrop: 'Ether Flux', itemId: 381, baseMobDropChance: 40.5, baseGoldDropChance: 100 },
        'Scarlet Merchant': { mobDrop: null, itemId: null, baseMobDropChance: 0, baseGoldDropChance: 100 },
        'Voidkin Artificier': { mobDrop: 'Void Artifact', itemId: 385, baseMobDropChance: 100, baseGoldDropChance: 100 },
        'Voidstalker': { mobDrop: 'Tattered Cowl', itemId: 386, baseMobDropChance: 40.5, baseGoldDropChance: 100 },
        'Tunnel Ambusher': { mobDrop: 'Carapace Segment', itemId: 387, baseMobDropChance: 40.5, baseGoldDropChance: 100 },
        'Elite Guard Broodmother': { mobDrop: 'Elite Guard Insignia', itemId: 388, baseMobDropChance: 40.5, baseGoldDropChance: 100 }
    };

    function createButton() {
        const button = document.createElement('button');
        button.textContent = 'Combat Calculator';
        button.style.cssText = "position: fixed; top: 20px; right: 20px; background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; z-index: 1000;";
        document.body.appendChild(button);
        button.addEventListener('click', toggleCalculator);
    }

    function createCalculatorUI() {
        const container = document.createElement('div');
        container.id = 'combatCalculator';
        container.style.cssText = "display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: black; color: white; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(255,255,255,0.5); z-index: 1001; max-height: 80vh; overflow-y: auto; min-width: 800px;";


        const title = document.createElement('h2');
        title.textContent = 'Combat Calculator';
        title.style.textAlign = 'center';

        const subtitle = document.createElement('h3');
        subtitle.style.textAlign = 'center';
        subtitle.innerHTML = 'Made by <a href="https://elethor.com/profile/49979" target="_blank">Eugene</a>';

        container.appendChild(title);
        container.appendChild(subtitle);

        // -- Uniform Settings Container --
        const uniformContainer = document.createElement('div');
        uniformContainer.id = 'uniformContainer';
        uniformContainer.style.cssText = "margin: 10px 0; padding: 10px; border: 1px solid white;";

        // Uniform Midas Checkbox
        const uniformMidasLabel = document.createElement('label');
        uniformMidasLabel.style.marginRight = '20px';
        const uniformMidasCheckbox = document.createElement('input');
        uniformMidasCheckbox.type = 'checkbox';
        uniformMidasCheckbox.id = 'uniformMidasCheckbox';
        uniformMidasLabel.appendChild(uniformMidasCheckbox);
        uniformMidasLabel.appendChild(document.createTextNode(' Uniform Midas'));
        uniformMidasCheckbox.addEventListener('change', function() {
            document.querySelectorAll('.midas-input').forEach(input => {
                input.disabled = this.checked;
                input.parentElement.classList.toggle('disabled-input', this.checked);
            });
        });
        uniformContainer.appendChild(uniformMidasLabel);

        // Uniform Extractor Checkbox
        const uniformExtractorLabel = document.createElement('label');
        const uniformExtractorCheckbox = document.createElement('input');
        uniformExtractorCheckbox.type = 'checkbox';
        uniformExtractorCheckbox.id = 'uniformExtractorCheckbox';
        uniformExtractorLabel.appendChild(uniformExtractorCheckbox);
        uniformExtractorLabel.appendChild(document.createTextNode(' Uniform Extractor'));
        uniformExtractorCheckbox.addEventListener('change', function() {
            document.querySelectorAll('.extractor-input').forEach(input => {
                input.disabled = this.checked;
                input.parentElement.classList.toggle('disabled-input', this.checked);
            });
        });
        uniformContainer.appendChild(uniformExtractorLabel);

        // Global Midas Input (hidden by default)
        const globalMidasContainer = document.createElement('div');
        globalMidasContainer.id = 'globalMidasContainer';
        globalMidasContainer.style.cssText = "display: none; margin-top: 10px;";
        const globalMidasLabel = document.createElement('label');
        globalMidasLabel.textContent = 'Global Midas %: ';
        const globalMidasInput = document.createElement('input');
        globalMidasInput.type = 'number';
        globalMidasInput.id = 'globalMidasInput';
        globalMidasInput.min = '0';
        globalMidasInput.style.cssText = "background-color: white; color: black;";
        globalMidasContainer.appendChild(globalMidasLabel);
        globalMidasContainer.appendChild(globalMidasInput);
        uniformContainer.appendChild(globalMidasContainer);

        // Global Extractor Input (hidden by default)
        const globalExtractorContainer = document.createElement('div');
        globalExtractorContainer.id = 'globalExtractorContainer';
        globalExtractorContainer.style.cssText = "display: none; margin-top: 10px;";
        const globalExtractorLabel = document.createElement('label');
        globalExtractorLabel.textContent = 'Global Extractor %: ';
        const globalExtractorInput = document.createElement('input');
        globalExtractorInput.type = 'number';
        globalExtractorInput.id = 'globalExtractorInput';
        globalExtractorInput.min = '0';
        globalExtractorInput.style.cssText = "background-color: white; color: black;";
        globalExtractorContainer.appendChild(globalExtractorLabel);
        globalExtractorContainer.appendChild(globalExtractorInput);
        uniformContainer.appendChild(globalExtractorContainer);

        // Event listeners for uniform checkboxes
        uniformMidasCheckbox.addEventListener('change', function() {
            if (this.checked) {
                globalMidasContainer.style.display = 'block';
                // Disable individual midas inputs
                document.querySelectorAll('.midas-input').forEach(input => {
                    input.disabled = true;
                    input.style.backgroundColor = '#f0f0f0';
                });
            } else {
                globalMidasContainer.style.display = 'none';
                // Enable individual midas inputs
                document.querySelectorAll('.midas-input').forEach(input => {
                    input.disabled = false;
                    input.style.backgroundColor = 'white';
                });
            }
        });

        uniformExtractorCheckbox.addEventListener('change', function() {
            if (this.checked) {
                globalExtractorContainer.style.display = 'block';
                // Disable individual extractor inputs
                document.querySelectorAll('.extractor-input').forEach(input => {
                    input.disabled = true;
                    input.style.backgroundColor = '#f0f0f0';
                });
            } else {
                globalExtractorContainer.style.display = 'none';
                document.querySelectorAll('.extractor-input').forEach(input => {
                    input.disabled = false;
                    input.style.backgroundColor = 'white';
                });
            }
        });

        container.appendChild(uniformContainer);
        const progressIndicator = document.createElement('div');
        progressIndicator.id = 'progressIndicator';
        progressIndicator.style.cssText = "margin-bottom: 10px; font-size: 14px;";
        container.appendChild(progressIndicator);
        // -------------------------------

        const monsterList = document.createElement('div');
        monsterList.style.marginTop = '20px';

        for (const monster in monsterData) {
            const row = createMonsterRow(monster, monsterData[monster]);
            monsterList.appendChild(row);
        }

        const calculateButton = document.createElement('button');
        calculateButton.textContent = 'Calculate';
        calculateButton.style.cssText = "background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-top: 20px; width: 100%;";
        calculateButton.addEventListener('click', calculateGpA);

        container.appendChild(monsterList);
        container.appendChild(calculateButton);
        document.body.appendChild(container);

        // Add overlay
        const overlay = document.createElement('div');
        overlay.id = 'calculatorOverlay';
        overlay.style.cssText = "display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 1000;";
        overlay.addEventListener('click', toggleCalculator);
        document.body.appendChild(overlay);
    }

    function createMonsterRow(monsterName, data) {
        const row = document.createElement('div');
        // Add a class to easily identify monster rows
        row.classList.add('monster-row');
        row.style.cssText = "display: grid; grid-template-columns: 200px repeat(6, 1fr) 100px; gap: 10px; margin-bottom: 15px; align-items: center; color: white;";

        // Monster name
        const nameLabel = document.createElement('div');
        nameLabel.textContent = monsterName;
        row.appendChild(nameLabel);

        // Tier input
        const tierInput = document.createElement('input');
        tierInput.type = 'number';
        tierInput.min = '0';
        tierInput.max = '10000';
        tierInput.placeholder = 'Tier';
        tierInput.className = 'tier-input';
        tierInput.style.backgroundColor = 'white';
        tierInput.style.color = 'black';
        row.appendChild(tierInput);

        // Win rate input
        const winRateInput = document.createElement('input');
        winRateInput.type = 'number';
        winRateInput.min = '0';
        winRateInput.max = '100';
        winRateInput.placeholder = 'Win Rate %';
        winRateInput.className = 'winrate-input';
        winRateInput.style.backgroundColor = 'white';
        winRateInput.style.color = 'black';
        row.appendChild(winRateInput);

        // Price radio buttons
        const priceContainer = document.createElement('div');
        priceContainer.style.cssText = "display: flex; align-items: center; gap: 10px;";
        const priceLabel = document.createElement('span');
        priceLabel.textContent = 'Price: ';
        priceContainer.appendChild(priceLabel);

        const priceTypes = ['Buy price', 'Sell price', 'Manual'];
        priceTypes.forEach((type, index) => {
            const radioContainer = document.createElement('div');
            radioContainer.style.cssText = "display: flex; align-items: center; gap: 4px;";

            const radio = document.createElement('input');
            radio.type = 'radio';
            radio.name = `price-${monsterName}`;
            radio.value = type.toLowerCase();
            radio.id = `${type.toLowerCase()}-${monsterName}`;
            radio.style.margin = '0';
            if (index === 0) radio.checked = true;

            const label = document.createElement('label');
            label.htmlFor = radio.id;
            label.textContent = type;
            label.style.margin = '0';

            radioContainer.appendChild(radio);
            radioContainer.appendChild(label);
            priceContainer.appendChild(radioContainer);
        });

        // Disable price options for Scarlet Merchant
        if (monsterName === 'Scarlet Merchant') {
            priceContainer.querySelectorAll('input[type="radio"]').forEach(radio => {
                radio.disabled = true;
            });
        }

        row.appendChild(priceContainer);

        // Price input - always visible and persistent
        const priceInput = document.createElement('input');
        priceInput.type = 'number';
        priceInput.min = '0';
        priceInput.placeholder = monsterName === 'Scarlet Merchant' ? 'No mob drops' : 'Price';
        priceInput.className = 'price-input';
        priceInput.dataset.monster = monsterName;
        priceInput.style.backgroundColor = monsterName === 'Scarlet Merchant' ? '#f0f0f0' : 'white';
        priceInput.style.color = 'black';
        if (monsterName === 'Scarlet Merchant') {
            priceInput.disabled = true;
        }
        row.appendChild(priceInput);

        // Load saved values from localStorage if available
        const savedManualPrice = localStorage.getItem(`manual-price-${monsterName}`);
        const savedTier = localStorage.getItem(`tier-${monsterName}`);
        const savedWinRate = localStorage.getItem(`winrate-${monsterName}`);

        if (savedManualPrice) {
            priceInput.value = savedManualPrice;
        }
        if (savedTier) {
            tierInput.value = savedTier;
        }
        if (savedWinRate) {
            winRateInput.value = savedWinRate;
        }

        // Save tier and winrate to localStorage when they change
        tierInput.addEventListener('input', () => {
            localStorage.setItem(`tier-${monsterName}`, tierInput.value);
        });

        winRateInput.addEventListener('input', () => {
            localStorage.setItem(`winrate-${monsterName}`, winRateInput.value);
        });

        // Save manual price to localStorage when it changes
        priceInput.addEventListener('input', () => {
            if (priceContainer.querySelector('input[value="manual"]:checked')) {
                localStorage.setItem(`manual-price-${monsterName}`, priceInput.value);
            }
        });

        // Handle price input behavior based on radio selection
        priceContainer.querySelectorAll('input[type="radio"]').forEach(radio => {
            // Load saved price type selection from localStorage
            const savedPriceType = localStorage.getItem(`price-type-${monsterName}`);
            if (savedPriceType && radio.value === savedPriceType) {
                radio.checked = true;
            }

            radio.addEventListener('change', () => {
                // Save price type selection to localStorage
                localStorage.setItem(`price-type-${monsterName}`, radio.value);

                if (radio.value === 'manual') {
                    priceInput.disabled = false;
                    priceInput.style.backgroundColor = 'white';
                    priceInput.placeholder = 'Enter price';
                    // Restore saved manual price if available
                    const savedPrice = localStorage.getItem(`manual-price-${monsterName}`);
                    if (savedPrice) {
                        priceInput.value = savedPrice;
                    }
                } else {
                    priceInput.disabled = true;
                    priceInput.style.backgroundColor = '#f0f0f0';
                    priceInput.placeholder = 'Fetching price...';
                    // If monster has an item, fetch the price immediately
                    if (data.itemId) {
                        fetchMarketData(data.itemId, radio.value === 'buy price' ? 'buy' : 'sell')
                            .then(price => {
                            priceInput.value = price;
                        })
                            .catch(error => {
                            console.error(`Error fetching price for ${monsterName}:`, error);
                            priceInput.placeholder = 'Error fetching price';
                        });
                    }
                }
            });
        });

        // Set initial state based on saved or default selection
        const selectedRadio = priceContainer.querySelector('input[type="radio"]:checked');
        if (selectedRadio.value === 'manual') {
            priceInput.disabled = false;
            priceInput.style.backgroundColor = 'white';
            priceInput.placeholder = 'Enter price';
        } else {
            priceInput.disabled = true;
            priceInput.style.backgroundColor = '#f0f0f0';
            priceInput.placeholder = 'Fetching price...';
            // If monster has an item, fetch the price immediately
            if (data.itemId) {
                fetchMarketData(data.itemId, selectedRadio.value === 'buy price' ? 'buy' : 'sell')
                    .then(price => {
                    priceInput.value = price;
                })
                    .catch(error => {
                    console.error(`Error fetching price for ${monsterName}:`, error);
                    priceInput.placeholder = 'Error fetching price';
                });
            }
        }

        // Midas percentage input
        const midasInput = document.createElement('input');
        midasInput.type = 'number';
        midasInput.min = '0';
        midasInput.placeholder = 'Midas %';
        midasInput.className = 'midas-input';
        midasInput.style.backgroundColor = 'white';
        midasInput.style.color = 'black';
        row.appendChild(midasInput);

        // Load saved Midas value
        const savedMidas = localStorage.getItem(`midas-${monsterName}`);
        if (savedMidas) {
            midasInput.value = savedMidas;
        }

        // Save Midas value when it changes
        midasInput.addEventListener('input', () => {
            localStorage.setItem(`midas-${monsterName}`, midasInput.value);
        });

        // Extractor percentage input
        const extractorInput = document.createElement('input');
        extractorInput.type = 'number';
        extractorInput.min = '0';
        extractorInput.placeholder = 'Extractor %';
        extractorInput.className = 'extractor-input';
        extractorInput.style.backgroundColor = 'white';
        extractorInput.style.color = 'black';
        row.appendChild(extractorInput);

        // Load saved Extractor value
        const savedExtractor = localStorage.getItem(`extractor-${monsterName}`);
        if (savedExtractor) {
            extractorInput.value = savedExtractor;
        }

        // Save Extractor value when it changes
        extractorInput.addEventListener('input', () => {
            localStorage.setItem(`extractor-${monsterName}`, extractorInput.value);
        });

        // GpA result
        const gpaResult = document.createElement('div');
        gpaResult.className = 'gpa-result';
        row.appendChild(gpaResult);

        return row;
    }

    async function fetchMarketData(itemId, priceType) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://elethor.com/game/market/listings?itemId=${itemId}`,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (!data || !Array.isArray(data)) {
                            throw new Error('Invalid market data received');
                        }

                        const relevantListings = data.filter(listing => listing.type === priceType);
                        if (relevantListings.length === 0) {
                            throw new Error(`No ${priceType} listings found`);
                        }

                        if (priceType === 'sell') {
                            resolve(Math.min(...relevantListings.map(listing => listing.price)));
                        } else {
                            resolve(Math.max(...relevantListings.map(listing => listing.price)));
                        }
                    } catch (error) {
                        reject(error);
                    }
                },
                onerror: function(error) {
                    reject(error);
                }
            });
        });
    }

    function calculateTierMultiplier(tier, isGold, isScarletMerchant) {
        if (isGold) {
            if (isScarletMerchant) {
                return 1 + (tier * 0.2);
            } else {
                return 1 + (tier * 0.15);
            }
        } else {
            return 1 + (tier * 0.75);
        }
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function calculateGpA() {
        const progressIndicator = document.getElementById('progressIndicator');
        const rows = document.querySelectorAll('.monster-row');
        let maxGpA = -1;
        let maxGpARow = null;

        // Reset previous results
        document.querySelectorAll('.gpa-result').forEach(result => {
            result.textContent = '';
            result.style.color = '';
        });

        // Step 1: Refresh market prices
        progressIndicator.textContent = "Step 1: Refreshing market prices...";
        let totalMonsters = rows.length;
        let count = 0;
        for (const row of rows) {
            const monsterName = row.querySelector('div').textContent;
            progressIndicator.textContent = `Fetching price for ${monsterName} (${++count}/${totalMonsters})...`;

            const monsterInfo = monsterData[monsterName];
            const selectedPriceType = row.querySelector('input[type="radio"]:checked')?.value;
            const priceInput = row.querySelector('.price-input');

            if (monsterInfo.itemId && selectedPriceType !== 'manual') {
                try {
                    const price = await fetchMarketData(
                        monsterInfo.itemId,
                        selectedPriceType === 'buy price' ? 'buy' : 'sell'
                    );
                    priceInput.value = price;
                } catch (error) {
                    console.error(`Error refreshing price for ${monsterName}:`, error);
                }
            }
            // Optional small delay to help the UI update the text
            await sleep(50);
        }

        // Step 2: Calculate GPA for each monster
        progressIndicator.textContent = "Step 2: Calculating GPAs...";
        count = 0;
        for (const row of rows) {
            const monsterName = row.querySelector('div').textContent;
            progressIndicator.textContent = `Calculating GPA for ${monsterName} (${++count}/${totalMonsters})...`;

            const monsterInfo = monsterData[monsterName];
            const tier = parseFloat(row.querySelector('.tier-input').value) || 0;
            const winRate = parseFloat(row.querySelector('.winrate-input').value) || 0;
            let midasPercentage = parseFloat(row.querySelector('.midas-input').value) || 0;
            let extractorPercentage = parseFloat(row.querySelector('.extractor-input').value) || 0;

            // Override with global values if uniform mode is enabled
            const uniformMidas = document.getElementById('uniformMidasCheckbox').checked;
            const uniformExtractor = document.getElementById('uniformExtractorCheckbox').checked;
            if (uniformMidas) {
                midasPercentage = parseFloat(document.getElementById('globalMidasInput').value) || 0;
            }
            if (uniformExtractor) {
                extractorPercentage = parseFloat(document.getElementById('globalExtractorInput').value) || 0;
            }

            let mobDropPrice = 0;
            const selectedPriceType = row.querySelector('input[type="radio"]:checked')?.value;

            if (monsterInfo.itemId && selectedPriceType !== 'manual') {
                try {
                    mobDropPrice = await fetchMarketData(
                        monsterInfo.itemId,
                        selectedPriceType === 'buy price' ? 'buy' : 'sell'
                    );
                } catch (error) {
                    console.error(`Error fetching price for ${monsterName}:`, error);
                }
            } else if (selectedPriceType === 'manual') {
                mobDropPrice = parseFloat(row.querySelector('.price-input').value) || 0;
            }

            const mobDropMultiplier = calculateTierMultiplier(tier, false, false);
            const goldMultiplier = calculateTierMultiplier(tier, true, monsterName === 'Scarlet Merchant');

            // Determine the gold multiplier factor based on the monster
const goldMultiplierFactor = (monsterName === 'Scarlet Merchant') ? 40850 : 24400;

const gpa = (
    ((winRate / 100) * monsterInfo.baseMobDropChance * mobDropMultiplier / 100) * (1 + extractorPercentage / 100) * mobDropPrice +
    (((winRate / 100) * monsterInfo.baseGoldDropChance * goldMultiplier / 100) * (1 + midasPercentage / 100)) * goldMultiplierFactor
);


            const gpaResult = row.querySelector('.gpa-result');
            gpaResult.textContent = gpa.toFixed(2);

            if (gpa > maxGpA) {
                maxGpA = gpa;
                maxGpARow = row;
            }
            // Optional delay for UI responsiveness
            await sleep(50);
        }

        // Step 3: Compare and mark best GPA
        progressIndicator.textContent = "Step 3: Comparing GPAs...";
        if (maxGpARow) {
            const checkmark = '✓';
            maxGpARow.querySelector('.gpa-result').style.color = '#4CAF50';
            maxGpARow.querySelector('.gpa-result').textContent += ` ${checkmark}`;
        }

        // Final update
        progressIndicator.textContent = "Calculation complete!";
    }


    function toggleCalculator() {
        const calculator = document.getElementById('combatCalculator');
        const overlay = document.getElementById('calculatorOverlay');
        const isVisible = calculator.style.display === 'block';

        calculator.style.display = isVisible ? 'none' : 'block';
        overlay.style.display = isVisible ? 'none' : 'block';
    }

    // Initialize the calculator
    createButton();
    createCalculatorUI();
})();