Gym Auto-Disabler

Prevents over-training of a gym. Requires torntools to take perks into account when training. Not tested on PDA/mobile.

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

// ==UserScript==
// @name         Gym Auto-Disabler
// @namespace    Titanic
// @version      v1.5
// @description  Prevents over-training of a gym. Requires torntools to take perks into account when training. Not tested on PDA/mobile.
// @author       Titanic_ [2968477]
// @match        https://*.torn.com/gym.php*
// @grant        none
// ==/UserScript==

const statNames = [ "Strength", "Speed", "Dexterity", "Defense" ];
const statConstants = {
    Strength: { A: 1600, B: 1700, C: 700 },
    Speed: { A: 1600, B: 2000, C: 1350 },
    Dexterity: { A: 1800, B: 1500, C: 1000 },
    Defense: { A: 2100, B: -600, C: 1500 },
    Energy: {},
    Happy: {},
    Gym: { Keep: [ // gyms you want to keep in this format: "quotes",
        "Frontline Fitness",
        "Elites",
    ]},
};

/*

  Valid gym names:

  "Balboas Gym",
  "Frontline Fitness",
  "Gym 3000",
  "Mr. Isoyamas",
  "Total Rebound",
  "Elites",

*/

function setBasicValues() {
    const energy = getStatValue("[class*=bar_][class*=energy_] > [class*=bar-stats] > [class*=bar-value]");
    const happy = getStatValue("[class*=bar_][class*=happy_] > [class*=bar-stats] > [class*=bar-value]");

    const selectedGymName = getSelectedGymName();
    if (!selectedGymName) return;

    const gymStats = getGymStatsByName(selectedGymName);
    if (!gymStats) return;

    statConstants.Energy.Total = Number(energy);
    statConstants.Happy.Total = Number(happy);
    statConstants.Gym.Energy = gymStats.energy;

    statConstants.Gym.Happy = {
        5: 2.67,
        10: 5,
        25: 12.67,
        50: 25
    }[statConstants.Gym.Energy] || 0;

    setStatValues(gymStats);
}

function calculateTrainingGain({
    statType,
    currentStat,
    startingHappy,
    gymMultiplier,
    energyPerTrain,
    perkMultiplier
}) {
    const { A, B, C } = statConstants[statType];
    perkMultiplier = (perkMultiplier ? perkMultiplier / 100 : 0) + 1

    if (currentStat > 50000000) {
        const baselog = Math.log(currentStat) / Math.log(10)
        currentStat = (currentStat - 50000000) / (8.77635 * baselog) + 50000000
    }

    const logValue = Math.log(1 + startingHappy / 250);
    const roundedLogValue = parseFloat(logValue.toFixed(4));
    const multiplier = 1 + 0.07 * parseFloat(roundedLogValue.toFixed(4));

    const baseGain = (
        (currentStat * multiplier +
        (8 * (startingHappy ** 1.05)) +
        ((1 - ((startingHappy / 99999) ** 2)) * A) +
        B) * (1 / 200000) * gymMultiplier * energyPerTrain
    );

    const totalGain = baseGain * perkMultiplier;
    return Number(totalGain.toFixed(2)); // to 2 decimal places
}

async function calculateAllTrainingGains() {
    const success = await waitForEl("[class*=propertyValue_]");
    if(!success) return

    setBasicValues();

    for (const stat of statNames) {
        if (!statConstants.Gym[stat]) continue; // skip stat if not exist in this gym

        const input = document.querySelector(`[class*=${stat.toLowerCase()}_] input`)
        const selectedTrains = input.value;
        const maxTrains = Math.floor(statConstants.Energy.Total / statConstants.Gym.Energy)
        const numTrains = selectedTrains

        let totalGains = 0;
        for (let i = 0; i < numTrains; i++) {
            const gains = calculateTrainingGain({
                statType: stat,
                currentStat: statConstants[stat].Total,
                startingHappy: Math.max(0, statConstants.Happy.Total - (i * statConstants.Gym.Happy)),
                gymMultiplier: statConstants.Gym[stat],
                energyPerTrain: statConstants.Gym.Energy,
                perkMultiplier: statConstants[stat].Perk
            });

            totalGains += gains;
        }

        statConstants[stat].Gain = totalGains;

        const button = document.querySelector(`[class*=${stat.toLowerCase()}_] .torn-btn`)
        if(!button) {
            alert("Could not find train button");
            return
        }
        const lock = checkRequirements(stat)

        if (lock) {
            console.log("Disabling",stat);
            button.disabled = true;
        }

        $(input).on("change", () => calculateAllTrainingGains());
        $(button).on("click", () => calculateAllTrainingGains());

    }
}

function checkRequirements(stat) {
    const stats = {
        strength: statConstants.Strength.Total,
        speed: statConstants.Speed.Total,
        dexterity: statConstants.Dexterity.Total,
        defense: statConstants.Defense.Total,
    };

    stats.strength += statConstants.Strength?.Gain || 0;
    stats.speed += statConstants.Speed?.Gain || 0;
    stats.dexterity += statConstants.Dexterity?.Gain || 0;
    stats.defense += statConstants.Defense?.Gain || 0;

    const gyms = statConstants.Gym.Keep;
    const secondStat = secondHighest(stats)

    let lock;
    for (const gym of gyms) {
        lock = true;

        switch (gym) {
            case 'Balboas Gym':
                if (stat == "Defense" || stat == "Dexterity") lock = false;
                else if ((stats.defense + stats.dexterity) >= 1.25 * (stats.speed + stats.strength)) {
                    lock = false;
                }
                break;
            case 'Frontline Fitness':
                if (stat == "Speed" || stat == "Strength") lock = false;
                else if ((stats.speed + stats.strength) >= 1.25 * (stats.defense + stats.dexterity)) {
                    lock = false;
                }
                break;
            case 'Gym 3000':
                if (stat == "Strength") lock = false;
                else if (stats.strength >= 1.25 * secondStat.value) {
                    lock = false;
                } else if (stat.toLowerCase() !== secondStat.name) {
                    lock = false;
                }
                break;
            case 'Mr. Isoyamas':
                if (stat == "Defense") lock = false;
                else if (stats.defense >= 1.25 * secondStat.value) {
                    lock = false;
                } else if (stat.toLowerCase() !== secondStat.name) {
                    lock = false;
                }
                break;
            case 'Total Rebound':
                if (stat == "Speed") lock = false;
                else if (stats.speed >= 1.25 * secondStat.value) {
                    lock = false;
                } else if (stat.toLowerCase() !== secondStat.name) {
                    lock = false;
                }
                break;
            case 'Elites':
                if (stat == "Dexterity") lock = false;
                else if (stats.dexterity >= 1.25 * secondStat.value) {
                    lock = false;
                } else if (stat.toLowerCase() !== secondStat.name) {
                    lock = false;
                }
                break;
            default:
                lock = true;
                break;
        }

        if (lock) break;
    }

    return lock;
}

calculateAllTrainingGains();

///////////////////////////////////////////////////////////////////////
////////////////////////////   UTILITY   //////////////////////////////
///////////////////////////////////////////////////////////////////////

function waitForEl(selector, timeout = 5000, delay = 1000) {
    return new Promise((resolve, reject) => {
        const observer = new MutationObserver((mutationsList, observer) => {
            const element = document.querySelector(selector);
            if (element) {
                observer.disconnect();
                setTimeout(() => resolve(element), delay);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });

        setTimeout(() => {
            observer.disconnect();
            reject(new Error(`Element with selector "${selector}" not found within the time limit`));
        }, timeout);
    });
}

// Helper to fetch and parse a stat value (energy or happiness)
function getStatValue(selector) {
    const value = document.querySelector(selector)?.textContent.match(/^\d+/)?.[0] || 0;
    return value;
}

// Get the selected gym's name
function getSelectedGymName() {
    const gymElement = document.querySelector("[class*=gymButton_][class*=active_]");
    if (!gymElement) {
        alert("Could not find selected gym");
        return null;
    }

    const aria = gymElement?.getAttribute("aria-label")?.match(/^(.+?)\./)?.[1].trim();
    if (!aria) {
        alert("Could not find gym's aria label")
        return null
    }

    return aria;
}

// Get gym stats by name
function getGymStatsByName(name) {
    for (const gym in gyms) {
        if(gyms[gym].name === name) {
            return gyms[gym];
            break;
        }
    }

    return null
}

// Set values for each stat based on the gym's capabilities
function setStatValues(gymStats) {
    let totalStats = 0;

    for (const stat of statNames) {
        const statDiv = document.querySelector(`[class*=${stat.toLowerCase()}_]`);
        if (!statDiv) {
            alert(`Could not find ${stat}.`);
            continue;
        }

        const value = parseInt(statDiv?.querySelector("[class*=propertyValue_]")?.textContent?.replace(/,/g, "") || -1, 10);
        if(value == -1) {
            alert("Could not find number of trains input")
        }
        const perk = parseFloat(statDiv?.querySelector(".tt-gym-steadfast")?.getAttribute("data-total") || 0);

        statConstants[stat].Total = value;
        statConstants[stat].Perk = perk;

        statConstants.Gym[stat] = gymStats[stat.toLowerCase()] / 10;
        totalStats += value;
    }

    for (const stat of statNames) {
        if (statConstants[stat].Total && totalStats) {
            statConstants[stat].Percent = (statConstants[stat].Total / totalStats) * 100;
        } else {
            statConstants[stat].Percent = 0;
        }
    }

    console.log("StatConstants", statConstants);
}

function secondHighest(statObj) {
    const sortedStats = Object.entries(statObj)
        .sort(([, aValue], [, bValue]) => bValue - aValue); // Sort by value, descending

    const [name, value] = sortedStats[1]; // Get the second entry (index 1)
    return { name, value };
}

////////////////////////////////////////////////////////////////////////
////////////////////////////   GYM DATA   //////////////////////////////
////////////////////////////////////////////////////////////////////////

const gyms = {
    "1": {
        "name": "Premier Fitness",
        "energy": 5,
        "strength": 20,
        "speed": 20,
        "defense": 20,
        "dexterity": 20,
    },
    "2": {
        "name": "Average Joes",
        "energy": 5,
        "strength": 24,
        "speed": 24,
        "defense": 27,
        "dexterity": 24,
    },
    "3": {
        "name": "Woody's Workout Club",
        "energy": 5,
        "strength": 27,
        "speed": 32,
        "defense": 30,
        "dexterity": 27,
    },
    "4": {
        "name": "Beach Bods",
        "energy": 5,
        "strength": 32,
        "speed": 32,
        "defense": 32,
        "dexterity": 0,
    },
    "5": {
        "name": "Silver Gym",
        "energy": 5,
        "strength": 34,
        "speed": 36,
        "defense": 34,
        "dexterity": 32,
    },
    "6": {
        "name": "Pour Femme",
        "energy": 5,
        "strength": 34,
        "speed": 36,
        "defense": 36,
        "dexterity": 38,
    },
    "7": {
        "name": "Davies Den",
        "energy": 5,
        "strength": 37,
        "speed": 0,
        "defense": 37,
        "dexterity": 37,
    },
    "8": {
        "name": "Global Gym",
        "energy": 5,
        "strength": 40,
        "speed": 40,
        "defense": 40,
        "dexterity": 40,
    },
    "9": {
        "name": "Knuckle Heads",
        "energy": 10,
        "strength": 48,
        "speed": 44,
        "defense": 40,
        "dexterity": 42,
    },
    "10": {
        "name": "Pioneer Fitness",
        "energy": 10,
        "strength": 44,
        "speed": 46,
        "defense": 48,
        "dexterity": 44,
    },
    "11": {
        "name": "Anabolic Anomalies",
        "energy": 10,
        "strength": 50,
        "speed": 46,
        "defense": 52,
        "dexterity": 46,
    },
    "12": {
        "name": "Core",
        "energy": 10,
        "strength": 50,
        "speed": 52,
        "defense": 50,
        "dexterity": 50,
    },
    "13": {
        "name": "Racing Fitness",
        "energy": 10,
        "strength": 50,
        "speed": 54,
        "defense": 48,
        "dexterity": 52,
    },
    "14": {
        "name": "Complete Cardio",
        "energy": 10,
        "strength": 55,
        "speed": 57,
        "defense": 55,
        "dexterity": 52,
    },
    "15": {
        "name": "Legs, Bums and Tums",
        "energy": 10,
        "strength": 0,
        "speed": 55,
        "defense": 55,
        "dexterity": 57,
    },
    "16": {
        "name": "Deep Burn",
        "energy": 10,
        "strength": 60,
        "speed": 60,
        "defense": 60,
        "dexterity": 60,
    },
    "17": {
        "name": "Apollo Gym",
        "energy": 10,
        "strength": 60,
        "speed": 62,
        "defense": 64,
        "dexterity": 62,
    },
    "18": {
        "name": "Gun Shop",
        "energy": 10,
        "strength": 65,
        "speed": 64,
        "defense": 62,
        "dexterity": 62,
    },
    "19": {
        "name": "Force Training",
        "energy": 10,
        "strength": 64,
        "speed": 65,
        "defense": 64,
        "dexterity": 68,
    },
    "20": {
        "name": "Cha Cha's",
        "energy": 10,
        "strength": 64,
        "speed": 64,
        "defense": 68,
        "dexterity": 70,
    },
    "21": {
        "name": "Atlas",
        "energy": 10,
        "strength": 70,
        "speed": 64,
        "defense": 64,
        "dexterity": 65,
    },
    "22": {
        "name": "Last Round",
        "energy": 10,
        "strength": 68,
        "speed": 65,
        "defense": 70,
        "dexterity": 65,
    },
    "23": {
        "name": "The Edge",
        "energy": 10,
        "strength": 68,
        "speed": 70,
        "defense": 70,
        "dexterity": 68,
    },
    "24": {
        "name": "George's",
        "energy": 10,
        "strength": 73,
        "speed": 73,
        "defense": 73,
        "dexterity": 73,
    },
    "25": {
        "name": "Balboas Gym",
        "energy": 25,
        "strength": 0,
        "speed": 0,
        "defense": 75,
        "dexterity": 75,
    },
    "26": {
        "name": "Frontline Fitness",
        "energy": 25,
        "strength": 75,
        "speed": 75,
        "defense": 0,
        "dexterity": 0,
    },
    "27": {
        "name": "Gym 3000",
        "energy": 50,
        "strength": 80,
        "speed": 0,
        "defense": 0,
        "dexterity": 0,
    },
    "28": {
        "name": "Mr. Isoyamas",
        "energy": 50,
        "strength": 0,
        "speed": 0,
        "defense": 80,
        "dexterity": 0,
    },
    "29": {
        "name": "Total Rebound",
        "energy": 50,
        "strength": 0,
        "speed": 80,
        "defense": 0,
        "dexterity": 0,
    },
    "30": {
        "name": "Elites",
        "energy": 50,
        "strength": 0,
        "speed": 0,
        "defense": 0,
        "dexterity": 80,
    },
    "31": {
        "name": "The Sports Science Lab",
        "energy": 25,
        "strength": 90,
        "speed": 90,
        "defense": 90,
        "dexterity": 90,
    },
    "32": {
        "name": "Unknown",
        "energy": 10,
        "strength": 100,
        "speed": 100,
        "defense": 100,
        "dexterity": 100,
    },
    "33": {
        "name": "The Jail Gym",
        "energy": 5,
        "strength": 34,
        "speed": 34,
        "defense": 46,
        "dexterity": 0,
    }
}