Greasy Fork is available in English.
Advanced gym analyzer with inline compact UI, live updates, custom programs and training advice
// ==UserScript==
// @name Torn Gym Analyzer
// @namespace torn.gym.analyzer
// @version 4.92
// @description Advanced gym analyzer with inline compact UI, live updates, custom programs and training advice
// @match https://www.torn.com/gym.php*
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ===== PROGRAMS =====
const programsList = [
{ name: 'None', str:0, def:0, spd:0, dex:0, is_custom:false },
{ name: 'All-Rounder', str:25, def:25, spd:25, dex:25, is_custom:false },
{ name: 'Strength Focus', str:40, def:20, spd:20, dex:20, is_custom:false },
{ name: 'Defense Focus', str:20, def:40, spd:20, dex:20, is_custom:false },
{ name: 'Speed Focus', str:20, def:20, spd:40, dex:20, is_custom:false },
{ name: 'Dexterity Focus', str:20, def:20, spd:20, dex:40, is_custom:false },
{ name: 'STR/DEF Build', str:35, def:35, spd:15, dex:15, is_custom:false },
{ name: 'SPD/DEX Build', str:15, def:15, spd:35, dex:35, is_custom:false },
{ name: "Hank's Ratio", str:27.78, def:34.72, spd:27.78, dex:9.72, is_custom:false },
{ name: "Baldr's Ratio", str:30.68, def:22.22, spd:24.69, dex:22.22, is_custom:false },
{ name: "Duce's Ratio", str:20, def:10, spd:40, dex:30, is_custom:false },
{ name: 'Custom', str:25, def:25, spd:25, dex:25, is_custom:true }
];
let selectedProgram = GM_getValue("program", "None");
// ===== CUSTOM PROGRAM =====
function getCustomProgram() {
return GM_getValue("customProgram", { str:25, def:25, spd:25, dex:25 });
}
function saveCustomProgram(p) {
GM_setValue("customProgram", p);
}
function normalizeProgram(p) {
const total = p.str + p.def + p.spd + p.dex || 1;
return {
str: (p.str / total) * 100,
def: (p.def / total) * 100,
spd: (p.spd / total) * 100,
dex: (p.dex / total) * 100
};
}
// ===== GET CURRENT STATS =====
function getStats() {
function getValue(statClass) {
const el = document.querySelector(`li[class*="${statClass}"] span[class*="propertyValue"]`);
if(!el) return 0;
return parseFloat(el.innerText.replace(/,/g,""));
}
return {
str: getValue("strength"),
def: getValue("defense"),
spd: getValue("speed"),
dex: getValue("dexterity")
};
}
// ===== INLINE UI =====
function renderInline(percentages,target) {
const statsList = ["str","def","spd","dex"];
statsList.forEach(stat=>{
const li = document.querySelector(`li[class*="${stat === "str" ? "strength" : stat === "def" ? "defense" : stat === "spd" ? "speed" : "dexterity"}"]`);
if(!li) return;
const rightPanel = li.querySelector('[class*="propertyContent"] > div:last-child');
if(!rightPanel) return;
let box = rightPanel.querySelector(".gym-inline-box");
if(!box){
box = document.createElement("div");
box.className="gym-inline-box";
box.style.fontSize="11px";
box.style.marginTop="6px";
box.style.textAlign="center";
box.style.opacity="0.9";
box.style.fontWeight="500";
box.style.letterSpacing="0.3px";
rightPanel.appendChild(box);
}
const diff = target[stat]-percentages[stat];
let color;
if(diff>20) color="#ff4d4d";
else if(diff>5) color="#ffb84d";
else color="#4dff88";
const total=Object.values(getStats()).reduce((a,b)=>a+b,0);
const statGoal = (target[stat]/100)*total;
let statDiff = statGoal - getStats()[stat];
statDiff = statDiff<0?0:statDiff;
const trainCount = Math.ceil(statDiff/10);
box.innerHTML = `
<span style="color:${color}">
${percentages[stat].toFixed(1)}% (${diff>0?"+":""}${diff.toFixed(1)}%)
${trainCount>0?`Train ~${trainCount}x`:""}
</span>
`;
});
}
// ===== HIGHLIGHT KNOPPEN =====
function highlightStats(percentages,target){
const diffs = Object.entries(percentages)
.map(([stat,val])=>({stat,diff:target[stat]-val}))
.sort((a,b)=>b.diff-a.diff);
const toTrain = diffs.filter(d=>d.diff>0);
const finalStats = toTrain.length?toTrain:[diffs[0]];
const buttons=document.querySelectorAll('button[aria-label^="Train"]');
buttons.forEach(btn=>{
btn.style.outline="";
btn.style.boxShadow="";
finalStats.forEach(s=>{
if(btn.getAttribute("aria-label").toLowerCase().includes(s.stat)){
btn.style.outline="2px solid #00ffcc";
btn.style.boxShadow="0 0 10px #00ffcc";
}
});
});
}
// ===== COMPACT DROPDOWN + CUSTOM =====
function addCompactSelector(){
if(document.getElementById("gym-compact-container")) return;
const container = document.createElement("div");
container.id="gym-compact-container";
container.style.display="flex";
container.style.alignItems="center";
container.style.gap="10px";
container.style.margin="10px 0";
// Label
const label = document.createElement("div");
label.innerText="Choose your training program:";
label.style.fontWeight="bold";
container.appendChild(label);
// Program dropdown
const select = document.createElement("select");
select.id="gym-program-select";
programsList.forEach(prog=>{
const opt = document.createElement("option");
opt.value = prog.name;
if(prog.name==="None" || prog.is_custom){
opt.textContent = prog.name;
} else {
const total = prog.str + prog.def + prog.spd + prog.dex;
const percentages = {
str: Math.round(prog.str/total*100),
def: Math.round(prog.def/total*100),
spd: Math.round(prog.spd/total*100),
dex: Math.round(prog.dex/total*100)
};
opt.textContent = `${prog.name} ${percentages.str}/${percentages.def}/${percentages.spd}/${percentages.dex}`;
}
if(prog.name===selectedProgram) opt.selected=true;
select.appendChild(opt);
});
select.onchange=()=>{
GM_setValue("program",select.value);
location.reload();
};
container.appendChild(select);
// Custom inputs inline if selected
if(selectedProgram==="Custom"){
const custom = getCustomProgram();
["str","def","spd","dex"].forEach(stat=>{
const input = document.createElement("input");
input.type="number";
input.id="c_"+stat;
input.value=custom[stat];
input.style.width="50px";
input.style.marginLeft="4px";
container.appendChild(input);
});
// Save icon
const saveBtn = document.createElement("button");
saveBtn.innerHTML="💾";
saveBtn.title="Save Custom Program";
saveBtn.style.fontSize="16px";
saveBtn.style.cursor="pointer";
saveBtn.style.background="transparent";
saveBtn.style.border="none";
saveBtn.style.color="#0ff";
saveBtn.onmouseover=()=>saveBtn.style.color="#fff";
saveBtn.onmouseout=()=>saveBtn.style.color="#0ff";
saveBtn.onclick=()=>{
const newProgram={
str: parseFloat(document.getElementById("c_str").value)||0,
def: parseFloat(document.getElementById("c_def").value)||0,
spd: parseFloat(document.getElementById("c_spd").value)||0,
dex: parseFloat(document.getElementById("c_dex").value)||0
};
saveCustomProgram(newProgram);
location.reload();
};
container.appendChild(saveBtn);
}
const root=document.querySelector("#gymroot");
if(root) root.prepend(container);
// Visual hint for "None"
if(selectedProgram==="None"){
const hint = document.createElement("div");
hint.innerText="⚠ No program selected. Please choose a training program to see advice.";
hint.style.color="#888";
hint.style.fontSize="12px";
hint.style.marginTop="4px";
container.appendChild(hint);
}
}
// ===== CORE =====
function runAnalysis(){
if(selectedProgram==="None") return; // geen programma geselecteerd -> niets tonen
const stats=getStats();
const total=Object.values(stats).reduce((a,b)=>a+b,0);
if(!total) return;
const percentages={};
for(let k in stats) percentages[k]=(stats[k]/total)*100;
let targetProg = programsList.find(p=>p.name===selectedProgram);
let target;
if(targetProg.is_custom) target=normalizeProgram(getCustomProgram());
else target=normalizeProgram(targetProg);
renderInline(percentages,target);
highlightStats(percentages,target);
}
// ===== LIVE =====
let timeout;
function observeChanges(){
const observer=new MutationObserver(()=>{
clearTimeout(timeout);
timeout=setTimeout(runAnalysis,200);
});
observer.observe(document.body,{childList:true,subtree:true});
}
// ===== WAIT =====
function waitForGym(){
const check=setInterval(()=>{
if(document.querySelector("#gymroot")){
clearInterval(check);
init();
}
},300);
}
// ===== INIT =====
function init(){
addCompactSelector();
runAnalysis();
observeChanges();
}
waitForGym();
})();