Torn Gym Ratio

Displays recommended stat training order based on your custom Str/Def/Spd/Dex ratios.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Gym Ratio
// @namespace    torn.gym.ratio
// @description  Displays recommended stat training order based on your custom Str/Def/Spd/Dex ratios.
// @version      1.0
// @match        https://www.torn.com/gym.php*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    const STATS = ['str', 'def', 'spd', 'dex'];
    const NAMES = { str: 'Strength', def: 'Defense', spd: 'Speed', dex: 'Dexterity' };
    const CLASSES = {
        str: 'strength___',
        def: 'defense___',
        spd: 'speed___',
        dex: 'dexterity___'
    };

    // Helper: Parse stat value from DOM
    function getStat(key) {
        const el = document.querySelector(`[class*="${CLASSES[key]}"] [class*="propertyValue___"]`);
        return el ? parseInt(el.textContent.replace(/,/g, '')) : 0;
    }

    // Helper: Check if stats are loaded
    function statsReady() {
        return STATS.every(s => getStat(s) > 0);
    }

    // Helper: Load settings (defaulting lock to 'auto')
    function load() {
        try {
            const defaults = { lock: 'auto', str: 1, def: 1, spd: 1, dex: 1 };
            const saved = JSON.parse(GM_getValue('gymRatio', '{}'));
            return { ...defaults, ...saved };
        } catch {
            return { lock: 'auto', str: 1, def: 1, spd: 1, dex: 1 };
        }
    }

    // Helper: Save settings
    function save(data) {
        GM_setValue('gymRatio', JSON.stringify(data));
    }

    // Main Calculation Logic
    function calculate() {
        if (!statsReady()) return;

        const data = load();
        let lock = data.lock;
        const rec = document.querySelector('#gr-rec');
        const current = {};

        // 1. Get current stats
        STATS.forEach(s => current[s] = getStat(s));

        // 2. Handle Auto-Lock Logic
        // If set to auto, find the stat that is "Furthest Ahead" relative to ratio
        if (lock === 'auto') {
            let maxScore = -1;
            let bestStat = null;

            STATS.forEach(s => {
                const ratio = data[s] || 0;
                if (ratio > 0) {
                    const score = current[s] / ratio;
                    if (score > maxScore) {
                        maxScore = score;
                        bestStat = s;
                    }
                }
            });
            lock = bestStat;
        }

        // If we still don't have a lock (e.g., all ratios are 0), stop.
        if (!lock) {
            if (rec) rec.textContent = 'Set ratios > 0';
            return;
        }

        // 3. Calculate Goals based on the Lock
        const base = current[lock] / (data[lock] || 1); // Avoid division by zero
        const needs = {};
        const ratios = {};

        STATS.forEach(s => {
            if (s === lock) {
                // The lock is our baseline (100% complete relative to itself)
                ratios[s] = 1;
                needs[s] = 0;
                return;
            }

            const target = base * (data[s] || 0);
            needs[s] = Math.ceil(target - current[s]);

            // Calculate completion ratio for sorting
            // If target is 0, we treat it as infinite completion (don't need to train it)
            ratios[s] = target > 0 ? current[s] / target : Infinity;
        });

        // 4. Sort stats by ratio (lowest = most behind = needs training)
        const sorted = STATS
            .filter(s => s !== lock && (data[s] || 0) > 0)
            .sort((a, b) => ratios[a] - ratios[b]);

        // 5. Update Grid UI (Needs / Checks)
        STATS.forEach(s => {
            const row = document.querySelector(`#gr-row-${s}`);
            const needEl = document.querySelector(`#gr-need-${s}`);
            if (!row || !needEl) return;

            if (s === lock) {
                needEl.innerHTML = `<span style="color:#999; font-weight:bold;">LOCKED</span>`;
                row.style.opacity = '0.5';
                row.style.borderColor = '#555';
            } else {
                row.style.opacity = '1';
                row.style.borderColor = '#333';

                const need = needs[s];
                if (need > 0) {
                    needEl.innerHTML = `<span style="color:#e07850">+${need.toLocaleString()}</span>`;
                } else {
                    const surplus = Math.abs(need).toLocaleString();
                    needEl.innerHTML = `<span style="color:#6c9">✓ +${surplus}</span>`;
                }
            }
        });

        // 6. Generate Recommendation Text
        if (rec) {
            // Check if we actually have deficits
            const worst = sorted[0];

            if (worst && needs[worst] > 0) {
                // We have a stat to train
                if (sorted.length >= 2) {
                    // Logic: Train worst until it matches second worst
                    const secondWorst = sorted[1];
                    const targetWorst = base * (data[worst] || 0);

                    // The formula: How much to gain until worst's ratio == secondWorst's ratio
                    const gainUntilSwitch = Math.ceil((targetWorst * ratios[secondWorst]) - current[worst]);

                    if (gainUntilSwitch > 0) {
                        rec.innerHTML = `Train <b>${NAMES[worst]}</b> <span style="color:#aaa">(+${gainUntilSwitch.toLocaleString()} → ${NAMES[secondWorst]})</span>`;
                    } else {
                        // Edge case: math says switch is close, just train worst
                        rec.innerHTML = `Train <b>${NAMES[worst]}</b>`;
                    }
                } else {
                    // Only 1 stat behind (or only 2 stats tracked total)
                    rec.innerHTML = `Train <b>${NAMES[worst]}</b>`;
                }
                rec.style.color = '#fc0'; // Orange/Yellow text
            } else {
                // Everything is balanced (or better than the lock)
                // In Auto mode, this technically shouldn't happen often unless perfectly balanced
                rec.innerHTML = `Balanced! Train <b>${NAMES[lock]}</b> (Lock)`;
                rec.style.color = '#6c9'; // Green text
            }
        }
    }

    // Build the User Interface
    function createUI() {
        if (document.querySelector('#gym-ratio')) return false;

        const gym = document.querySelector('[class*="gymContent___"]');
        if (!gym) return false;

        const data = load();
        const container = document.createElement('div');
        container.id = 'gym-ratio';
        container.innerHTML = `
            <style>
                #gym-ratio {
                    background: linear-gradient(to bottom, #242424, #1a1a1a);
                    border: 1px solid #444;
                    border-radius: 5px;
                    margin-bottom: 10px;
                    font-family: Arial, sans-serif;
                    font-size: 12px;
                    color: #ccc;
                }
                #gr-header {
                    background: linear-gradient(to bottom, #333, #2a2a2a);
                    padding: 8px 12px;
                    border-bottom: 1px solid #444;
                    font-size: 13px;
                    font-weight: bold;
                    color: #fff;
                }
                #gr-body { padding: 10px 12px; }
                #gr-grid {
                    display: grid;
                    grid-template-columns: repeat(4, 1fr);
                    gap: 8px;
                    margin-bottom: 10px;
                }
                .gr-stat {
                    background: #1a1a1a;
                    border: 1px solid #333;
                    border-radius: 4px;
                    padding: 8px;
                    text-align: center;
                    transition: opacity 0.3s;
                }
                .gr-stat-name {
                    font-size: 11px;
                    color: #888;
                    text-transform: uppercase;
                    margin-bottom: 4px;
                }
                .gr-stat-input {
                    width: 40px;
                    background: #111;
                    border: 1px solid #444;
                    border-radius: 3px;
                    color: #fff;
                    text-align: center;
                    padding: 4px;
                    font-size: 13px;
                    margin-bottom: 6px;
                }
                .gr-stat-input:focus { border-color: #69c; outline: none; }
                .gr-stat-need { font-size: 11px; min-height: 16px; white-space: nowrap; }
                #gr-footer {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding-top: 8px;
                    border-top: 1px solid #333;
                }
                #gr-lock-wrap { display: flex; align-items: center; gap: 6px; }
                #gr-lock {
                    background: #111;
                    border: 1px solid #444;
                    border-radius: 3px;
                    color: #fff;
                    padding: 4px 8px;
                    font-size: 12px;
                    cursor: pointer;
                }
                #gr-rec { font-size: 13px; }
            </style>
            <div id="gr-header">Gym Ratio Trainer</div>
            <div id="gr-body">
                <div id="gr-grid">
                    ${STATS.map(s => `
                        <div class="gr-stat" id="gr-row-${s}">
                            <div class="gr-stat-name">${NAMES[s]}</div>
                            <input type="number" class="gr-stat-input" id="gr-${s}" value="${data[s] !== undefined ? data[s] : 1}" min="0">
                            <div class="gr-stat-need" id="gr-need-${s}">-</div>
                        </div>
                    `).join('')}
                </div>
                <div id="gr-footer">
                    <div id="gr-lock-wrap">
                        <span>Lock:</span>
                        <select id="gr-lock">
                            <option value="auto" ${data.lock === 'auto' ? 'selected' : ''}>Auto</option>
                            ${STATS.map(s => `<option value="${s}"${data.lock === s ? ' selected' : ''}>${NAMES[s]}</option>`).join('')}
                        </select>
                    </div>
                    <div id="gr-rec">Initializing...</div>
                </div>
            </div>
        `;

        gym.parentNode.insertBefore(container, gym);

        // Add event listeners to inputs and select
        container.querySelectorAll('input, select').forEach(el => {
            el.addEventListener('input', () => {
                const d = {};
                STATS.forEach(s => d[s] = parseFloat(document.querySelector(`#gr-${s}`).value) || 0);
                d.lock = document.querySelector('#gr-lock').value;
                save(d);
                calculate();
            });
        });

        return true;
    }

    // Init Logic: Wait for elements to exist
    function init() {
        const gym = document.querySelector('[class*="gymContent___"]');
        if (!gym) return;

        createUI();
        if (statsReady()) calculate();
    }

    // Start-up Interval
    let attempts = 0;
    const interval = setInterval(() => {
        init();
        attempts++;
        if (statsReady() && document.querySelector('#gym-ratio')) {
            calculate();
            clearInterval(interval);
        }
        if (attempts > 50) clearInterval(interval);
    }, 200);

    // Watch for stat updates (polling)
    let lastStats = '';
    setInterval(() => {
        if (!statsReady()) return;
        const current = STATS.map(s => getStat(s)).join(',');
        if (current !== lastStats) {
            lastStats = current;
            calculate();
        }
    }, 1000);

})();