TornPDA - Gym Gains Calculator

Embeds a collapsible gym gains calculator with stat goal tracking and enhanced outputs

目前為 2025-03-21 提交的版本,檢視 最新版本

// ==UserScript==
// @name         TornPDA - Gym Gains Calculator
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Embeds a collapsible gym gains calculator with stat goal tracking and enhanced outputs
// @author       Jvmie[2094564]
// @match        https://www.torn.com/gym.php*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

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

    function calculateSingleGain(stat, happy, gymDots, bonusMultiplier) {
        const baseGain = (gymDots * (0.00019106 * stat + 0.00226263 * happy + 0.55) * 4) / 13.06;
        return baseGain * bonusMultiplier;
    }

    function calculateTotalGain(stat, initialHappy, gymDots, numTrains, bonusMultiplier) {
        let totalGain = 0;
        let currentHappy = initialHappy;
        let currentStat = stat;

        for (let i = 0; i < numTrains; i++) {
            const gain = calculateSingleGain(currentStat, currentHappy, gymDots, bonusMultiplier);
            totalGain += gain;
            currentStat += gain;
            currentHappy -= 5; // Adjusted to match game log
            if (currentHappy < 0) currentHappy = 0;
        }

        return { singleGain: calculateSingleGain(stat, initialHappy, gymDots, bonusMultiplier), totalGain };
    }

    function calculateTrainsToGoal(currentStat, goalStat, initialHappy, gymDots, bonusMultiplier, energyPerTrain, sessionEnergy) {
        let totalGain = 0;
        let currentHappy = initialHappy;
        let currentStatValue = currentStat;
        let repsNeeded = 0;

        while (totalGain < (goalStat - currentStat) && currentHappy > 0) {
            const gain = calculateSingleGain(currentStatValue, currentHappy, gymDots, bonusMultiplier);
            totalGain += gain;
            currentStatValue += gain;
            currentHappy -= 5; // Adjusted to match game log
            if (currentHappy < 0) currentHappy = 0;
            repsNeeded++;
        }

        const energyRequired = repsNeeded * energyPerTrain;
        const trainsNeeded = Math.ceil(energyRequired / sessionEnergy);
        return { trainsNeeded, energyRequired, totalGain };
    }

    const factionBonuses = Array.from({ length: 19 }, (_, i) => i);
    const propertyBonuses = [0, 1, 2];
    const educationBonuses = [0, 1, 2];

    const calculatorHTML = `
        <div id="gymCalcContainer" style="margin: 10px; font-family: 'Arial', sans-serif; color: #fff;">
            <button id="collapseBtn" style="width: 100%; padding: 10px; background: #1a1a1a; color: #fff; border: none; cursor: pointer; text-align: left; font-size: 16px; font-weight: bold;">
                Gym Gains Calculator ▼
            </button>
            <div id="calcContent" style="display: none; padding: 15px; background: #2a2a2a; border: 1px solid #444; border-radius: 5px;">
                <div style="display: flex; flex-wrap: wrap; gap: 10px;">
                    <div style="flex: 1; min-width: 200px;">
                        <label style="font-size: 14px; color: #ccc;">Stat to Train: 
                            <select id="statSelect" style="width: 100%; padding: 5px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px;">
                                <option value="Str">Strength</option>
                                <option value="Spe">Speed</option>
                                <option value="Def">Defense</option>
                                <option value="Dex">Dexterity</option>
                            </select>
                        </label>
                    </div>
                    <div style="flex: 1; min-width: 200px;">
                        <label style="font-size: 14px; color: #ccc;">Gym: 
                            <select id="gymSelect" style="width: 100%; padding: 5px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px;">
                                ${Object.keys(gyms).map(gym => `<option value="${gym}" ${gym === "Gun Shop" ? "selected" : ""}>${gym}</option>`).join('')}
                            </select>
                        </label>
                    </div>
                </div>
                <div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px;">
                    <div style="flex: 1; min-width: 200px;">
                        <label style="font-size: 14px; color: #ccc;">Current Stat: 
                            <input type="number" id="currentStat" min="0" value="0" style="width: 100%; padding: 5px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px;">
                        </label>
                    </div>
                    <div style="flex: 1; min-width: 200px;">
                        <label style="font-size: 14px; color: #ccc;">Goal Stat: 
                            <input type="number" id="goalStat" min="0" value="0" style="width: 100%; padding: 5px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px;">
                        </label>
                    </div>
                </div>
                <div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px;">
                    <div style="flex: 1; min-width: 200px;">
                        <label style="font-size: 14px; color: #ccc;">Happiness: 
                            <input type="number" id="happy" min="0" value="0" style="width: 100%; padding: 5px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px;">
                        </label>
                    </div>
                    <div style="flex: 1; min-width: 200px;">
                        <label style="font-size: 14px; color: #ccc;">Total Energy: 
                            <input type="number" id="energy" min="0" value="0" style="width: 100%; padding: 5px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px;">
                        </label>
                    </div>
                </div>
                <div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px;">
                    <div style="flex: 1; min-width: 200px;">
                        <label style="font-size: 14px; color: #ccc;">Faction Bonus (%): 
                            <select id="factionBonus" style="width: 100%; padding: 5px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px;">
                                ${factionBonuses.map(val => `<option value="${val}" ${val === 0 ? "selected" : ""}>${val}%</option>`).join('')}
                            </select>
                        </label>
                    </div>
                    <div style="flex: 1; min-width: 200px;">
                        <label style="font-size: 14px; color: #ccc;">Property Bonus (%): 
                            <select id="propBonus" style="width: 100%; padding: 5px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px;">
                                ${propertyBonuses.map(val => `<option value="${val}" ${val === 0 ? "selected" : ""}>${val}%</option>`).join('')}
                            </select>
                        </label>
                    </div>
                </div>
                <div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px;">
                    <div style="flex: 1; min-width: 200px;">
                        <label style="font-size: 14px; color: #ccc;">Education Bonus (%): 
                            <select id="eduBonus" style="width: 100%; padding: 5px; background: #333; color: #fff; border: 1px solid #555; border-radius: 3px;">
                                ${educationBonuses.map(val => `<option value="${val}" ${val === 0 ? "selected" : ""}>${val}%</option>`).join('')}
                            </select>
                        </label>
                    </div>
                </div>
                <button id="calcBtn" style="margin-top: 15px; padding: 8px 15px; background: #4CAF50; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 14px;">Calculate</button>
                <div id="result" style="margin-top: 15px; font-size: 14px; color: #fff;"></div>
            </div>
        </div>
    `;

    const gymPage = document.querySelector('.content-wrapper');
    if (gymPage) {
        gymPage.insertAdjacentHTML('afterbegin', calculatorHTML);

        const collapseBtn = document.getElementById('collapseBtn');
        const calcContent = document.getElementById('calcContent');
        collapseBtn.addEventListener('click', () => {
            if (calcContent.style.display === 'none') {
                calcContent.style.display = 'block';
                collapseBtn.textContent = 'Gym Gains Calculator ▲';
            } else {
                calcContent.style.display = 'none';
                collapseBtn.textContent = 'Gym Gains Calculator ▼';
            }
        });

        const saveToLocalStorage = (key, value) => {
            localStorage.setItem(`gymCalc_${key}`, value);
        };

        const loadFromLocalStorage = (key, defaultValue) => {
            return localStorage.getItem(`gymCalc_${key}`) || defaultValue;
        };

        const statSelect = document.getElementById('statSelect');
        const gymSelect = document.getElementById('gymSelect');
        const currentStat = document.getElementById('currentStat');
        const goalStat = document.getElementById('goalStat');
        const happy = document.getElementById('happy');
        const energy = document.getElementById('energy');
        const factionBonus = document.getElementById('factionBonus');
        const propBonus = document.getElementById('propBonus');
        const eduBonus = document.getElementById('eduBonus');

        statSelect.value = loadFromLocalStorage('statSelect', 'Str');
        gymSelect.value = loadFromLocalStorage('gymSelect', 'Gun Shop');
        currentStat.value = loadFromLocalStorage('currentStat', '0');
        goalStat.value = loadFromLocalStorage('goalStat', '0');
        happy.value = loadFromLocalStorage('happy', '0');
        energy.value = loadFromLocalStorage('energy', '0');
        factionBonus.value = loadFromLocalStorage('factionBonus', '0');
        propBonus.value = loadFromLocalStorage('propBonus', '0');
        eduBonus.value = loadFromLocalStorage('eduBonus', '0');

        statSelect.addEventListener('change', () => saveToLocalStorage('statSelect', statSelect.value));
        gymSelect.addEventListener('change', () => saveToLocalStorage('gymSelect', gymSelect.value));
        currentStat.addEventListener('input', () => saveToLocalStorage('currentStat', currentStat.value));
        goalStat.addEventListener('input', () => saveToLocalStorage('goalStat', goalStat.value));
        happy.addEventListener('input', () => saveToLocalStorage('happy', happy.value));
        energy.addEventListener('input', () => saveToLocalStorage('energy', energy.value));
        factionBonus.addEventListener('change', () => saveToLocalStorage('factionBonus', factionBonus.value));
        propBonus.addEventListener('change', () => saveToLocalStorage('propBonus', propBonus.value));
        eduBonus.addEventListener('change', () => saveToLocalStorage('eduBonus', eduBonus.value));

        document.getElementById('calcBtn').addEventListener('click', () => {
            const statKey = statSelect.value;
            const gymName = gymSelect.value;
            const currentStatValue = parseFloat(currentStat.value) || 0;
            const goalStatValue = parseFloat(goalStat.value) || 0;
            const happyValue = parseFloat(happy.value) || 0;
            const energyValue = parseFloat(energy.value) || 0;
            const factionBonusValue = (parseFloat(factionBonus.value) || 0) / 100;
            const propBonusValue = (parseFloat(propBonus.value) || 0) / 100;
            const eduBonusValue = (parseFloat(eduBonus.value) || 0) / 100;

            const gym = gyms[gymName];
            const gymDots = gym[statKey];
            const energyPerTrain = gym.Energy;
            const numReps = Math.floor(energyValue / energyPerTrain);
            const bonusMultiplier = (1 + factionBonusValue) * (1 + propBonusValue) * (1 + eduBonusValue);

            if (gymDots === 0) {
                document.getElementById('result').innerHTML = `<span style="color: #ff5555;">This gym does not train ${statKey}!</span>`;
                return;
            }

            const { singleGain, totalGain } = calculateTotalGain(currentStatValue, happyValue, gymDots, numReps, bonusMultiplier);
            const statDifference = goalStatValue - currentStatValue;
            const sessionReps = numReps;
            let goalResults = '';
            
            if (goalStatValue > currentStatValue) {
                const { trainsNeeded, energyRequired, totalGain: goalGain } = calculateTrainsToGoal(
                    currentStatValue, goalStatValue, happyValue, gymDots, bonusMultiplier, energyPerTrain, energyValue
                );
                goalResults = `
                    <br><b style="color: #4CAF50;">Goal Progress:</b><br>
                    Stat Difference: ${statDifference.toFixed(2)}<br>
                    Energy Required: ${energyRequired}<br>
                    Training Sessions Required: ${trainsNeeded}<br>
                    Expected Gain to Goal: ${goalGain.toFixed(2)}
                `;
            }

            document.getElementById('result').innerHTML = `
                <b style="color: #4CAF50;">Results:</b><br>
                Single Rep Gain: ${singleGain.toFixed(2)}<br>
                Total Reps: ${numReps}<br>
                Training Sessions: 1<br>
                Total Gain: ${totalGain.toFixed(2)}
                ${goalResults}
            `;
        });
    }
})();