TornPDA - Gym Gains Calculator

A calculator for Torn's gym training, predicting stat gains, time to reach goals, and booster costs. Features a collapsible UI with bonus settings, energy/happiness boosters, and persistent user inputs.

// ==UserScript==
// @name         TornPDA - Gym Gains Calculator
// @namespace    http://tampermonkey.net/
// @version      2.53
// @description  A calculator for Torn's gym training, predicting stat gains, time to reach goals, and booster costs. Features a collapsible UI with bonus settings, energy/happiness boosters, and persistent user inputs.
// @author       Jvmie[2094564]
// @match        https://www.torn.com/gym.php*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // Gym data
    const gyms = {
        "[H] Gun Shop": { energy: 10, strength: 6.6, speed: 6.4, defense: 6.2, dexterity: 6.2 },
        "[L] Premier Fitness": { energy: 5, strength: 2, speed: 2, defense: 2, dexterity: 2 },
        "[L] Average Joes": { energy: 5, strength: 2.4, speed: 2.4, defense: 2.8, dexterity: 2.4 },
        "[L] Woody's Workout": { energy: 5, strength: 2.8, speed: 3.2, defense: 3, dexterity: 2.8 },
        "[L] Beach Bods": { energy: 5, strength: 3.2, speed: 3.2, defense: 3.2, dexterity: 0 },
        "[L] Silver Gym": { energy: 5, strength: 3.4, speed: 3.6, defense: 3.4, dexterity: 3.2 },
        "[L] Pour Femme": { energy: 5, strength: 3.4, speed: 3.6, defense: 3.6, dexterity: 3.8 },
        "[L] Davies Den": { energy: 5, strength: 3.7, speed: 0, defense: 3.7, dexterity: 3.7 },
        "[L] Global Gym": { energy: 5, strength: 4, speed: 4, defense: 4, dexterity: 4 },
        "[M] Knuckle Heads": { energy: 10, strength: 4.8, speed: 4.4, defense: 4, dexterity: 4.2 },
        "[M] Pioneer Fitness": { energy: 10, strength: 4.4, speed: 4.6, defense: 4.8, dexterity: 4.4 },
        "[M] Anabolic Anomalies": { energy: 10, strength: 5, speed: 4.6, defense: 5.2, dexterity: 4.6 },
        "[M] Core": { energy: 10, strength: 5, speed: 5.2, defense: 5, dexterity: 5 },
        "[M] Racing Fitness": { energy: 10, strength: 5, speed: 5.4, defense: 4.8, dexterity: 5.2 },
        "[M] Complete Cardio": { energy: 10, strength: 5.5, speed: 5.8, defense: 5.5, dexterity: 5.2 },
        "[M] Legs, Bums and Tums": { energy: 10, strength: 0, speed: 5.6, defense: 5.6, dexterity: 5.8 },
        "[M] Deep Burn": { energy: 10, strength: 6, speed: 6, defense: 6, dexterity: 6 },
        "[H] Apollo Gym": { energy: 10, strength: 6, speed: 6.2, defense: 6.4, dexterity: 6.2 },
        "[H] Force Training": { energy: 10, strength: 6.4, speed: 6.6, defense: 6.4, dexterity: 6.8 },
        "[H] Cha Cha's": { energy: 10, strength: 6.4, speed: 6.4, defense: 6.8, dexterity: 7 },
        "[H] Atlas": { energy: 10, strength: 7, speed: 6.4, defense: 6.4, dexterity: 6.6 },
        "[H] Last Round": { energy: 10, strength: 6.8, speed: 6.6, defense: 7, dexterity: 6.6 },
        "[H] The Edge": { energy: 10, strength: 6.8, speed: 7, defense: 7, dexterity: 6.8 },
        "[H] George's": { energy: 10, strength: 7.3, speed: 7.3, defense: 7.3, dexterity: 7.3 },
        "[S] Balboas Gym": { energy: 25, strength: 0, speed: 0, defense: 7.5, dexterity: 7.5 },
        "[S] Frontline Fitness": { energy: 25, strength: 7.5, speed: 7.5, defense: 0, dexterity: 0 },
        "[S] Gym 3000": { energy: 50, strength: 8, speed: 0, defense: 0, dexterity: 0 },
        "[S] Mr. Isoyamas": { energy: 50, strength: 0, speed: 0, defense: 8, dexterity: 0 },
        "[S] Total Rebound": { energy: 50, strength: 0, speed: 8, defense: 0, dexterity: 0 },
        "[S] Elites": { energy: 50, strength: 0, speed: 0, defense: 0, dexterity: 8 },
        "[S] Sports Science Lab": { energy: 25, strength: 9, speed: 9, defense: 9, dexterity: 9 },
        "The Jail Gym": { energy: 5, strength: 3.4, speed: 3.4, defense: 4.6, dexterity: 0 }
    };

    const stats = ["Strength", "Speed", "Defense", "Dexterity"];
    const bonusPercentages = Array.from({ length: 101 }, (_, i) => i); // 0 to 100

    // CSS for the embedded collapsible menu
    const styles = `
        .grok-menu {
            margin: 10px 0;
            background: #1a1a1a;
            border: 1px solid #444;
            border-radius: 5px;
            padding: 10px;
            font-family: "Arial", sans-serif;
            color: #ccc;
        }
        .grok-menu h3 {
            margin: 0;
            padding: 10px;
            background: linear-gradient(to bottom, #333, #222);
            cursor: pointer;
            border-radius: 3px;
            font-size: 16px;
            font-weight: bold;
            color: #ddd;
            border: 1px solid #555;
            transition: background 0.2s ease;
        }
        .grok-menu h3:hover {
            background: linear-gradient(to bottom, #444, #333);
        }
        .grok-content {
            display: none;
            padding: 10px;
        }
        .grok-content.show {
            display: block;
        }
        .grok-menu label {
            display: block;
            margin: 8px 0;
            font-size: 14px;
            color: #ddd;
        }
        .grok-menu input, .grok-menu select {
            width: 100%;
            padding: 5px;
            margin-top: 2px;
            border: 1px solid #444;
            border-radius: 3px;
            background: #333;
            color: #fff;
            box-sizing: border-box;
            font-size: 14px;
        }
        .grok-menu input:focus, .grok-menu select:focus {
            outline: none;
            border-color: #666;
        }
        .grok-menu .result {
            margin-top: 10px;
            padding: 8px;
            border-radius: 3px;
            font-size: 14px;
        }
        .grok-menu .result.red {
            background: #3a1c1c;
            color: #ff6666;
        }
        .grok-menu .result.grey {
            background: #2a2a2a;
            color: #bbb;
        }
        .grok-menu .button-container {
            margin-top: 10px;
            display: flex;
            gap: 10px;
        }
        .grok-menu button {
            background: #2a2a2a;
            border: 1px solid #444;
            border-radius: 3px;
            padding: 5px 10px;
            color: #fff;
            cursor: pointer;
            font-size: 14px;
            transition: background 0.2s ease;
        }
        .grok-menu button:hover {
            background: #3a3a3a;
        }
        .bonus-menu {
            margin: 10px 0;
            background: #1a1a1a;
            border: 1px solid #444;
            border-radius: 3px;
        }
        .bonus-menu h4 {
            margin: 0;
            padding: 8px;
            background: linear-gradient(to bottom, #333, #222);
            cursor: pointer;
            border-radius: 3px;
            font-size: 14px;
            font-weight: bold;
            color: #ddd;
            border: 1px solid #555;
            transition: background 0.2s ease;
        }
        .bonus-menu h4:hover {
            background: linear-gradient(to bottom, #444, #333);
        }
        .bonus-content {
            display: none;
            padding: 8px;
        }
        .bonus-content.show {
            display: block;
        }
        .changelog-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            z-index: 1000;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .changelog-box {
            background: #1a1a1a;
            border: 1px solid #444;
            border-radius: 5px;
            padding: 20px;
            max-width: 400px;
            color: #ccc;
            font-family: "Arial", sans-serif;
        }
        .changelog-box h4 {
            margin: 0 0 10px;
            color: #fff;
        }
        .changelog-box ul {
            margin: 0 0 20px;
            padding-left: 20px;
        }
        .changelog-box button {
            background: #2a2a2a;
            border: 1px solid #444;
            border-radius: 3px;
            padding: 5px 10px;
            color: #fff;
            cursor: pointer;
        }
        .changelog-box button:hover {
            background: #3a3a3a;
        }
    `;

    // Add styles to the page
    const styleSheet = document.createElement("style");
    styleSheet.textContent = styles;
    document.head.appendChild(styleSheet);

    // Find the main content area
    const contentWrapper = document.querySelector('.content-wrapper') || document.body;
    if (!contentWrapper) {
        console.error("Could not find content wrapper to embed calculator.");
        return;
    }

    // Load saved values
    const savedGym = GM_getValue("gym", "[H] Gun Shop");
    const savedStat = GM_getValue("stat", "strength");
    const savedEnergyBooster = GM_getValue("energyBooster", "none");
    const savedBoosterCount = GM_getValue("boosterCount", "0");
    const savedHappy = GM_getValue("happy", "4525");
    const savedStatTotal = GM_getValue("statTotal", "234522");
    const savedStatGoal = GM_getValue("statGoal", "300000");
    const savedEnergy = GM_getValue("energy", "10");
    const savedFactionPerk = GM_getValue("factionPerk", "0");
    const savedPropertyPerk = GM_getValue("propertyPerk", "0");
    const savedEduStatPerk = GM_getValue("eduStatPerk", "0");
    const savedEduGenPerk = GM_getValue("eduGenPerk", "0");
    const savedJobPerk = GM_getValue("jobPerk", "0");
    const savedBookPerk = GM_getValue("bookPerk", "0");
    const savedSportsSneakers = GM_getValue("sportsSneakers", "0");
    const savedSteroids = GM_getValue("steroids", "0");
    const savedEcstasy = GM_getValue("ecstasy", "no");
    const savedEroticDVDs = GM_getValue("eroticDVDs", "0");

    // Create the menu HTML with saved values
    const menu = document.createElement("div");
    menu.className = "grok-menu";
    menu.innerHTML = `
        <h3>TornPDA - Gym Gains Calculator</h3>
        <div class="grok-content">
            <label>Gym:
                <select id="gymSelect"></select>
            </label>
            <label>Stat to Train:
                <select id="statSelect">
                    ${stats.map(stat => `<option value="${stat.toLowerCase()}">${stat}</option>`).join('')}
                </select>
            </label>
            <label>Energy Booster:
                <select id="energyBooster">
                    <option value="none">None</option>
                    <option value="xanax">Xanax (+250 Energy, $880,000, 7 hr cooldown)</option>
                    <option value="energyCan">Energy Can (+20 Energy, $1,166,667, 30 min cooldown)</option>
                    <option value="fhc">Feathery Hotel Coupon (150 Energy, $12,500,000, 24 hr cooldown)</option>
                    <option value="refill">Energy Refill (150 Energy, $1,725,000, No cooldown)</option>
                </select>
            </label>
            <label>Number of Energy Boosters per Day:
                <input type="number" id="boosterCount" value="${savedBoosterCount}" min="0">
            </label>
            <label>Starting Happy: <input type="number" id="happy" value="${savedHappy}" min="0"></label>
            <label>Current Stat Total: <input type="number" id="statTotal" value="${savedStatTotal}" min="0"></label>
            <label>Desired Stat Goal: <input type="number" id="statGoal" value="${savedStatGoal}" min="0"></label>
            <label>Total Energy to Spend (Initial): <input type="number" id="energy" value="${savedEnergy}" min="0"></label>
            <div class="bonus-menu">
                <h4>Bonuses & Boosters</h4>
                <div class="bonus-content">
                    <label>Faction Steadfast (%):
                        <select id="factionPerk">
                            ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')}
                        </select>
                    </label>
                    <label>Property Perks (%):
                        <select id="propertyPerk">
                            ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')}
                        </select>
                    </label>
                    <label>Education (Stat Specific) (%):
                        <select id="eduStatPerk">
                            ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')}
                        </select>
                    </label>
                    <label>Education (General) (%):
                        <select id="eduGeneralPerk">
                            ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')}
                        </select>
                    </label>
                    <label>Job Perks (%):
                        <select id="jobPerk">
                            ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')}
                        </select>
                    </label>
                    <label>Book Perks (%):
                        <select id="bookPerk">
                            ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')}
                        </select>
                    </label>
                    <label>Sports Sneakers (Speed Only, %):
                        <select id="sportsSneakers">
                            <option value="0">0%</option>
                            <option value="5">5%</option>
                        </select>
                    </label>
                    <label>Steroids Booster (%):
                        <select id="steroids">
                            <option value="0">0%</option>
                            <option value="20">20%</option>
                        </select>
                    </label>
                    <label>Ecstasy (Doubles Happiness):
                        <select id="ecstasy">
                            <option value="no">No</option>
                            <option value="yes">Yes</option>
                        </select>
                    </label>
                    <label>Erotic DVDs (Happiness Boost):
                        <select id="eroticDVDs">
                            <option value="0">0</option>
                            <option value="1">1 (+2500 Happy)</option>
                            <option value="2">2 (+5000 Happy)</option>
                            <option value="3">3 (+7500 Happy)</option>
                            <option value="4">4 (+10000 Happy)</option>
                        </select>
                    </label>
                </div>
            </div>
            <div class="button-container">
                <button id="calculateButton">Calculate</button>
                <button id="copyButton" style="display: none;">Copy Results</button>
            </div>
            <div class="result grey">Energy Per Train: <span id="energyPerTrain">-</span></div>
            <div class="result grey">Number of Trains: <span id="numTrains">-</span></div>
            <div class="result grey">Bonus Multiplier: <span id="bonusMultiplier">-</span></div>
            <div class="result red">Predicted Gains (Single Train): <span id="singleGain">-</span></div>
            <div class="result red">Predicted Gains (Total Initial): <span id="totalGain">-</span></div>
            <div class="result red">Allowable Error (+/-): <span id="errorMargin">-</span></div>
            <div class="result grey">Total Energy Per Day: <span id="dailyEnergy">-</span></div>
            <div class="result grey">Total Cost of Boosters: <span id="boosterCost">-</span></div>
            <div class="result grey">Days to Reach Goal: <span id="daysToGoal">-</span></div>
            <div class="result grey">Total Booster Cost to Goal: <span id="totalBoosterCost">-</span></div>
        </div>
    `;

    // Populate gym dropdown and set saved value
    const gymSelect = menu.querySelector("#gymSelect");
    Object.keys(gyms).forEach(gym => {
        const option = document.createElement("option");
        option.value = gym;
        option.textContent = gym;
        gymSelect.appendChild(option);
    });
    gymSelect.value = savedGym;

    // Set saved values for other inputs
    const statSelect = menu.querySelector("#statSelect");
    statSelect.value = savedStat;

    const energyBoosterSelect = menu.querySelector("#energyBooster");
    energyBoosterSelect.value = savedEnergyBooster;

    const factionPerkSelect = menu.querySelector("#factionPerk");
    factionPerkSelect.value = savedFactionPerk;

    const propertyPerkSelect = menu.querySelector("#propertyPerk");
    propertyPerkSelect.value = savedPropertyPerk;

    const eduStatPerkSelect = menu.querySelector("#eduStatPerk");
    eduStatPerkSelect.value = savedEduStatPerk;

    const eduGenPerkSelect = menu.querySelector("#eduGeneralPerk");
    eduGenPerkSelect.value = savedEduGenPerk;

    const jobPerkSelect = menu.querySelector("#jobPerk");
    jobPerkSelect.value = savedJobPerk;

    const bookPerkSelect = menu.querySelector("#bookPerk");
    bookPerkSelect.value = savedBookPerk;

    const sportsSneakersSelect = menu.querySelector("#sportsSneakers");
    sportsSneakersSelect.value = savedSportsSneakers;

    const steroidsSelect = menu.querySelector("#steroids");
    steroidsSelect.value = savedSteroids;

    const ecstasySelect = menu.querySelector("#ecstasy");
    ecstasySelect.value = savedEcstasy;

    const eroticDVDsSelect = menu.querySelector("#eroticDVDs");
    eroticDVDsSelect.value = savedEroticDVDs;

    // Insert the menu
    contentWrapper.insertBefore(menu, contentWrapper.firstChild);

    // Toggle main menu visibility
    const header = menu.querySelector("h3");
    const content = menu.querySelector(".grok-content");
    header.addEventListener("click", () => {
        content.classList.toggle("show");
    });

    // Toggle bonus menu visibility
    const bonusHeader = menu.querySelector(".bonus-menu h4");
    const bonusContent = menu.querySelector(".bonus-content");
    bonusHeader.addEventListener("click", () => {
        bonusContent.classList.toggle("show");
    });

    // Save input values on change
    gymSelect.addEventListener("change", () => GM_setValue("gym", gymSelect.value));
    statSelect.addEventListener("change", () => GM_setValue("stat", statSelect.value));
    energyBoosterSelect.addEventListener("change", () => GM_setValue("energyBooster", energyBoosterSelect.value));
    factionPerkSelect.addEventListener("change", () => GM_setValue("factionPerk", factionPerkSelect.value));
    propertyPerkSelect.addEventListener("change", () => GM_setValue("propertyPerk", propertyPerkSelect.value));
    eduStatPerkSelect.addEventListener("change", () => GM_setValue("eduStatPerk", eduStatPerkSelect.value));
    eduGenPerkSelect.addEventListener("change", () => GM_setValue("eduGeneralPerk", eduGenPerkSelect.value));
    jobPerkSelect.addEventListener("change", () => GM_setValue("jobPerk", jobPerkSelect.value));
    bookPerkSelect.addEventListener("change", () => GM_setValue("bookPerk", bookPerkSelect.value));
    sportsSneakersSelect.addEventListener("change", () => GM_setValue("sportsSneakers", sportsSneakersSelect.value));
    steroidsSelect.addEventListener("change", () => GM_setValue("steroids", steroidsSelect.value));
    ecstasySelect.addEventListener("change", () => GM_setValue("ecstasy", ecstasySelect.value));
    eroticDVDsSelect.addEventListener("change", () => GM_setValue("eroticDVDs", eroticDVDsSelect.value));

    const boosterCountInput = menu.querySelector("#boosterCount");
    boosterCountInput.addEventListener("change", () => GM_setValue("boosterCount", boosterCountInput.value));

    const happyInput = menu.querySelector("#happy");
    happyInput.addEventListener("change", () => GM_setValue("happy", happyInput.value));

    const statTotalInput = menu.querySelector("#statTotal");
    statTotalInput.addEventListener("change", () => GM_setValue("statTotal", statTotalInput.value));

    const statGoalInput = menu.querySelector("#statGoal");
    statGoalInput.addEventListener("change", () => GM_setValue("statGoal", statGoalInput.value));

    const energyInput = menu.querySelector("#energy");
    energyInput.addEventListener("change", () => GM_setValue("energy", energyInput.value));

    // Calculation function with error handling
    function calculateGains() {
        try {
            const gym = gymSelect.value;
            const stat = statSelect.value;
            const gymData = gyms[gym];
            if (!gymData) throw new Error("Invalid gym selected");

            const energyPerTrain = gymData.energy;
            const gymDots = gymData[stat];
            if (!gymDots) throw new Error("Invalid stat for this gym");

            let happy = parseFloat(document.getElementById("happy").value) || 0;
            let statTotal = parseFloat(document.getElementById("statTotal").value) || 0;
            const statGoal = parseFloat(document.getElementById("statGoal").value) || statTotal;
            const totalEnergy = parseFloat(document.getElementById("energy").value) || 0;

            // Perks
            const factionPerk = (parseFloat(document.getElementById("factionPerk").value) || 0) / 100;
            const propertyPerk = (parseFloat(document.getElementById("propertyPerk").value) || 0) / 100;
            const eduStatPerk = (parseFloat(document.getElementById("eduStatPerk").value) || 0) / 100;
            const eduGenPerk = (parseFloat(document.getElementById("eduGeneralPerk").value) || 0) / 100;
            const jobPerk = (parseFloat(document.getElementById("jobPerk").value) || 0) / 100;
            const bookPerk = (parseFloat(document.getElementById("bookPerk").value) || 0) / 100;
            const sportsSneakers = (parseFloat(document.getElementById("sportsSneakers").value) || 0) / 100;
            const steroids = (parseFloat(document.getElementById("steroids").value) || 0) / 100;
            const ecstasy = document.getElementById("ecstasy").value;
            const eroticDVDs = parseInt(document.getElementById("eroticDVDs").value) || 0;
            const energyBooster = document.getElementById("energyBooster").value;
            let boosterCount = parseInt(document.getElementById("boosterCount").value) || 0;

            // Apply happiness boosters
            if (ecstasy === "yes") {
                happy *= 2;
            }
            happy += eroticDVDs * 2500;

            // Bonus multiplier
            let bonusMultiplier = (1 + factionPerk) * (1 + propertyPerk) * (1 + eduStatPerk) *
                                 (1 + eduGenPerk) * (1 + jobPerk) * (1 + bookPerk) * (1 + steroids);
            if (stat === "speed") {
                bonusMultiplier *= (1 + sportsSneakers);
            }

            // Number of trains (initial)
            const numTrains = Math.floor(totalEnergy / energyPerTrain);

            // Calculate single train gain
            const initialCoreComponent = (0.00019106 * statTotal) + (0.00226263 * happy) + 0.55;
            const initialBaseGain = (gymDots * 4) * initialCoreComponent;
            const singleGain = (initialBaseGain * bonusMultiplier / 147.24) * energyPerTrain;

            // Iterative calculation for initial energy
            let totalGain = 0;
            let currentStat = statTotal;
            let currentHappy = happy;
            for (let i = 0; i < numTrains; i++) {
                const coreComponent = (0.00019106 * currentStat) + (0.00226263 * currentHappy) + 0.55;
                const baseGain = (gymDots * 4) * coreComponent;
                const trainGain = (baseGain * bonusMultiplier / 147.24) * energyPerTrain;
                totalGain += trainGain;
                currentStat += trainGain;
                currentHappy = Math.max(0, currentHappy - (energyPerTrain * 0.5));
            }

            // Allowable error
            const errorMarginSingle = singleGain * 0.00233;
            const errorMarginTotal = totalGain * 0.00419;

            // Calculate daily energy with boosters
            let dailyEnergy = 480; // Natural energy (20 per hour * 24 hours)
            let boosterEnergy = 0;
            let boosterCostPerDay = 0;
            let cooldownHours = 0;
            let maxBoostersPerDay = 0;

            if (energyBooster === "xanax") {
                boosterEnergy = 250;
                boosterCostPerDay = boosterCount * 880000;
                cooldownHours = 7;
                maxBoostersPerDay = Math.floor(24 / cooldownHours); // 3 Xanax per day
            } else if (energyBooster === "energyCan") {
                boosterEnergy = 20;
                boosterCostPerDay = boosterCount * 1166667;
                cooldownHours = 0.5; // 30 minutes
                maxBoostersPerDay = 48; // Max 48 cans in 24 hours
            } else if (energyBooster === "fhc") {
                boosterEnergy = 150;
                boosterCostPerDay = boosterCount * 12500000;
                cooldownHours = 24;
                maxBoostersPerDay = 1; // 1 FHC per day
            } else if (energyBooster === "refill") {
                boosterEnergy = 150;
                boosterCostPerDay = boosterCount * 1725000;
                cooldownHours = 0; // No cooldown
                maxBoostersPerDay = 999; // Arbitrary high limit
            }

            boosterCount = Math.min(boosterCount, maxBoostersPerDay);
            dailyEnergy += boosterCount * boosterEnergy;

            // Add happiness booster costs
            if (ecstasy === "yes") {
                boosterCostPerDay += 100000; // 1 Ecstasy per day
            }
            boosterCostPerDay += eroticDVDs * 2500000; // Cost of Erotic DVDs
            if (steroids > 0) {
                boosterCostPerDay += 1000000; // 1 Steroids per day
            }

            // Calculate daily gains
            const dailyTrains = Math.floor(dailyEnergy / energyPerTrain);
            let dailyGain = 0;
            currentStat = statTotal + totalGain; // Start from after initial train
            currentHappy = happy; // Reset daily with Ecstasy
            for (let i = 0; i < dailyTrains; i++) {
                const coreComponent = (0.00019106 * currentStat) + (0.00226263 * currentHappy) + 0.55;
                const baseGain = (gymDots * 4) * coreComponent;
                const trainGain = (baseGain * bonusMultiplier / 147.24) * energyPerTrain;
                dailyGain += trainGain;
                currentStat += trainGain;
                currentHappy = Math.max(0, currentHappy - (energyPerTrain * 0.5));
            }

            // Calculate days to reach goal
            let daysToGoal = 0;
            let totalBoosterCost = 0;
            const maxIterations = 10000; // Prevent infinite loops
            let iterationCount = 0;

            while (currentStat < statGoal && iterationCount < maxIterations) {
                currentStat += dailyGain;
                daysToGoal++;
                totalBoosterCost += boosterCostPerDay;
                iterationCount++;
            }

            // Update display
            document.getElementById("energyPerTrain").textContent = energyPerTrain;
            document.getElementById("numTrains").textContent = numTrains;
            document.getElementById("bonusMultiplier").textContent = bonusMultiplier.toFixed(4);
            document.getElementById("singleGain").textContent = `${singleGain.toFixed(2)} (Min: ${(singleGain - errorMarginSingle).toFixed(2)}, Max: ${(singleGain + errorMarginSingle).toFixed(2)})`;
            document.getElementById("totalGain").textContent = `${totalGain.toFixed(2)} (Min: ${(totalGain - errorMarginTotal).toFixed(2)}, Max: ${(totalGain + errorMarginTotal).toFixed(2)})`;
            document.getElementById("errorMargin").textContent = `Single: ±${errorMarginSingle.toFixed(2)}, Total: ±${errorMarginTotal.toFixed(2)}`;
            document.getElementById("dailyEnergy").textContent = dailyEnergy;
            document.getElementById("boosterCost").textContent = `$${boosterCostPerDay.toLocaleString()}`;
            document.getElementById("daysToGoal").textContent = iterationCount >= maxIterations ? "Goal unreachable" : daysToGoal;
            document.getElementById("totalBoosterCost").textContent = iterationCount >= maxIterations ? "N/A" : `$${totalBoosterCost.toLocaleString()}`;

            // Show copy button
            document.getElementById("copyButton").style.display = "inline-block";
        } catch (error) {
            console.error("Calculation error:", error);
            alert("An error occurred during calculation: " + error.message);
            // Display default values to ensure output shows
            document.getElementById("energyPerTrain").textContent = "-";
            document.getElementById("numTrains").textContent = "-";
            document.getElementById("bonusMultiplier").textContent = "-";
            document.getElementById("singleGain").textContent = "-";
            document.getElementById("totalGain").textContent = "-";
            document.getElementById("errorMargin").textContent = "-";
            document.getElementById("dailyEnergy").textContent = "-";
            document.getElementById("boosterCost").textContent = "-";
            document.getElementById("daysToGoal").textContent = "-";
            document.getElementById("totalBoosterCost").textContent = "-";
        }
    }

    // Copy results to clipboard
    function copyResults() {
        try {
            const singleGain = document.getElementById("singleGain").textContent;
            const totalGain = document.getElementById("totalGain").textContent;
            const errorMargin = document.getElementById("errorMargin").textContent;
            const dailyEnergy = document.getElementById("dailyEnergy").textContent;
            const boosterCost = document.getElementById("boosterCost").textContent;
            const daysToGoal = document.getElementById("daysToGoal").textContent;
            const totalBoosterCost = document.getElementById("totalBoosterCost").textContent;

            const textToCopy = `Predicted Gains (Single Train): ${singleGain}\n` +
                               `Predicted Gains (Total Initial): ${totalGain}\n` +
                               `Allowable Error (+/-): ${errorMargin}\n` +
                               `Total Energy Per Day: ${dailyEnergy}\n` +
                               `Daily Booster Cost: ${boosterCost}\n` +
                               `Days to Reach Goal: ${daysToGoal}\n` +
                               `Total Booster Cost to Goal: ${totalBoosterCost}`;

            navigator.clipboard.writeText(textToCopy).then(() => {
                alert("Results copied to clipboard!");
            }).catch(err => {
                console.error("Failed to copy: ", err);
                alert("Failed to copy results. Please copy manually.");
            });
        } catch (error) {
            console.error("Copy error:", error);
            alert("An error occurred while copying results: " + error.message);
        }
    }

    // Add event listeners
    const calculateButton = menu.querySelector("#calculateButton");
    calculateButton.addEventListener("click", calculateGains);

    const copyButton = menu.querySelector("#copyButton");
    copyButton.addEventListener("click", copyResults);

    // Changelog Pop-up Logic
    const currentVersion = "2.53";
    const lastSeenVersion = GM_getValue("lastSeenVersion", "0.0");

    if (lastSeenVersion !== currentVersion) {
        const changelogOverlay = document.createElement("div");
        changelogOverlay.className = "changelog-overlay";
        changelogOverlay.innerHTML = `
            <div class="changelog-box">
                <h4>TornPDA - Gym Gains Calculator v${currentVersion}</h4>
                <ul>
                    <li>Added persistence for user entries on page close/refresh using GM_setValue/GM_getValue.</li>
                    <li>Previous: Improved styling of dropdown headers to match Torn's theme.</li>
                    <li>Previous: Moved Bonuses & Boosters menu to the bottom of the main UI.</li>
                    <li>Previous: Fixed TypeError by changing const to let for boosterCount.</li>
                    <li>Previous: Ensured bonuses are in a nested collapsible menu.</li>
                    <li>Previous: Added error handling to ensure output displays.</li>
                    <li>Previous: Added energy and happiness booster dropdowns.</li>
                    <li>Previous: Added desired stat goal input.</li>
                    <li>Previous: Calculated total time to goal with cooldowns.</li>
                    <li>Previous: Added booster cost calculations.</li>
                </ul>
                <button id="closeChangelog">Close</button>
            </div>
        `;
        document.body.appendChild(changelogOverlay);

        const closeButton = changelogOverlay.querySelector("#closeChangelog");
        closeButton.addEventListener("click", () => {
            GM_setValue("lastSeenVersion", currentVersion);
            changelogOverlay.remove();
        });
    }
})();