Torn Gym Ratio

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();