Torn Gym Analyzer

Advanced gym analyzer with inline compact UI, live updates, custom programs and training advice

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
})();