// ==UserScript==
// @name TornPDA - Gym Gains Calculator
// @namespace http://tampermonkey.net/
// @version 2.16
// @description Embeds a collapsible gym gains calculator with stat goal tracking, enhanced outputs, and a copy button
// @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;
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;
const statDifference = goalStat - currentStat;
while (totalGain < statDifference) {
const gain = calculateSingleGain(currentStatValue, currentHappy, gymDots, bonusMultiplier);
if (totalGain + gain >= statDifference) {
const remainingGain = statDifference - totalGain;
const fractionOfRep = remainingGain / gain;
repsNeeded += fractionOfRep;
totalGain = statDifference;
break;
}
totalGain += gain;
currentStatValue += gain;
currentHappy -= 5;
if (currentHappy < 0) currentHappy = 0;
repsNeeded++;
}
const energyRequired = Math.ceil(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];
// One-time pop-up message on script load
const currentVersion = '2.16';
const hasSeenUpdate = localStorage.getItem('gymCalcUpdateSeen') === currentVersion;
if (!hasSeenUpdate) {
// Define the changelog messages
const fullChangelog = `
- Removed HTML styling from copied text (v2.8).
- Fixed Goal Progress calculations to continue training even at 0 happiness (v2.8).
- Renamed "Expected Gain to Goal" to "Projected Gains" for clarity (v2.12).
- Updated "Projected Gains" to reflect gains from input energy instead of stat difference (v2.13).
- Removed "Projected Gains" from Goal Progress as it was redundant with "Total Gain" in Results (v2.14).
- Replaced Goal Progress with "Percentage of Goal Achieved" metric (v2.15).
- Added Desired Goal, Energy Used, and Happiness to both displayed Results and copied text (v2.16).
`;
const currentVersionChangelog = `
- Added Desired Goal, Energy Used, and Happiness to both displayed Results and copied text (v2.16).
`;
// Determine which changelog to show
const changelogToShow = currentVersion === '2.15' ? fullChangelog : currentVersionChangelog;
// Create the modal HTML
const modalHTML = `
<div id="updateModal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000;">
<div style="background: #2a2a2a; padding: 20px; border-radius: 5px; max-width: 90%; max-height: 80%; overflow-y: auto; color: #fff; font-family: 'Arial', sans-serif;">
<h2 style="margin-top: 0;">TornPDA - Gym Gains Calculator v${currentVersion}</h2>
<h3>Update Notes:</h3>
<pre style="white-space: pre-wrap; font-size: 14px;">${changelogToShow.trim()}</pre>
<div style="text-align: center; margin-top: 20px;">
<button id="closeModalBtn" style="padding: 10px 20px; background: #4CAF50; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 16px; font-weight: bold;">OK</button>
</div>
</div>
</div>
`;
// Insert the modal into the document
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Add event listener to close the modal
const closeModalBtn = document.getElementById('closeModalBtn');
closeModalBtn.addEventListener('click', () => {
const modal = document.getElementById('updateModal');
modal.remove();
localStorage.setItem('gymCalcUpdateSeen', currentVersion);
});
}
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>
<div style="display: flex; gap: 10px; margin-top: 15px;">
<button id="calcBtn" style="padding: 8px 15px; background: #4CAF50; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 14px;">Calculate</button>
<button id="copyBtn" style="padding: 8px 15px; background: #2196F3; color: #fff; border: none; border-radius: 3px; cursor: pointer; font-size: 14px;">Copy</button>
</div>
<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));
let lastResult = '';
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) {
lastResult = `This gym does not train ${statKey}!`;
document.getElementById('result').innerHTML = `<span style="color: #ff5555;">${lastResult}</span>`;
return;
}
const { singleGain, totalGain } = calculateTotalGain(currentStatValue, happyValue, gymDots, numReps, bonusMultiplier);
const statDifference = goalStatValue - currentStatValue;
const energyUsed = numReps * energyPerTrain;
let goalResultsPlain = '';
let goalResultsStyled = '';
if (goalStatValue > currentStatValue) {
// Calculate percentage of goal achieved
const percentageAchieved = (totalGain / statDifference) * 100;
// Plain text version for copying
goalResultsPlain = `
Goal Progress:
Percentage Achieved: ${percentageAchieved.toFixed(2)}%`;
// Styled version for display
goalResultsStyled = `
<b style="color: #4CAF50;">Goal Progress:</b><br>
Percentage Achieved: ${percentageAchieved.toFixed(2)}%`;
}
// Input summary for both display and copy
const inputSummaryPlain = `
Input Summary:
Desired Goal: ${goalStatValue.toFixed(2)}
Energy Used: ${energyUsed}
Happiness: ${happyValue.toFixed(2)}`;
const inputSummaryStyled = `
<b style="color: #4CAF50;">Input Summary:</b><br>
Desired Goal: ${goalStatValue.toFixed(2)}<br>
Energy Used: ${energyUsed}<br>
Happiness: ${happyValue.toFixed(2)}`;
// Plain text for copying
lastResult = `Results:
Single Rep Gain: ${singleGain.toFixed(2)}
Total Reps: ${numReps}
Training Sessions: 1
Total Gain: ${totalGain.toFixed(2)}${inputSummaryPlain}${goalResultsPlain}`;
// Styled text for display
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)}<br>
${inputSummaryStyled}<br>
${goalResultsStyled}`;
});
document.getElementById('copyBtn').addEventListener('click', () => {
if (lastResult) {
navigator.clipboard.writeText(lastResult).then(() => {
alert('Results copied to clipboard!');
}).catch(err => {
alert('Failed to copy results: ' + err);
});
} else {
alert('No results to copy! Please calculate first.');
}
});
}
})();