// ==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,
}
}