Personal Stat Average & Comparison

Shows daily averages for different stats, users, and time periods

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         Personal Stat Average & Comparison
// @namespace    ben_hagen.torn.personalstataverageandcomparison
// @version      1.0.5
// @author       Ben_Hagen [2966467] 
// @description  Shows daily averages for different stats, users, and time periods
// @license      GNU GPLv3
// @match        https://www.torn.com/personalstats.php*
// @grant        unsafeWindow

// ==/UserScript==


(function () {
    'use strict';

    const fmt = (n, decimals = 2) => {
        const parts = n.toFixed(decimals).split('.');
        parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
        return parts.join('.');
    };

    const formatAvg = (statName, avg) => {
        if (['Time played', 'Time spent traveling'].includes(statName))
            return `${fmt(avg / 60)} min/day`;
        if (['Total networth','Rehabilitation fees','Value of received bounties',
             'Money rewarded','Spent on bounties','Money mugged'].includes(statName))
            return `$${fmt(avg)}/day`;
        return `${fmt(avg)}/day`;
    };

    const parseValue = (str) => parseFloat(str.replace(/,/g, '')) || 0;

    let debounceTimer = null;
    let lastPollKey = '';
    let personalStats = null;

    const getAvgFromJson = (statName, userName) => {
        if (!personalStats?.definitions || !personalStats?.data) return null;
        const statKey = Object.keys(personalStats.definitions).find(k => personalStats.definitions[k] === statName);
        const userKey = Object.keys(personalStats.definitions).find(k => personalStats.definitions[k] === userName);
        if (!statKey || !userKey) return null;
        const series = personalStats.data[statKey]?.find(u => String(u.uid) === String(userKey));
        if (!series?.data?.length || series.data.length < 2) return null;
        const points = series.data; // newest first
        const daysDelta = (new Date(points[0].time * 1000) - new Date(points[points.length - 1].time * 1000)) / 86_400_000;
        if (daysDelta <= 0) return null;
        return (points[0].value - points[points.length - 1].value) / daysDelta;
    };

    const getAvgFromTable = (rows, colIndex) => {
        const values = rows.map(row => {
            const cells = row.querySelectorAll('td');
            return cells[colIndex + 1] ? parseValue(cells[colIndex + 1].textContent) : null;
        }).filter(v => v !== null);
        if (values.length < 2) return null;
        return (values[values.length - 1] - values[0]) / (values.length - 1);
    };

    const injectAverages = () => {
        const anchor = document.querySelector('[class^="dropDowns"]');
        if (!anchor) return;

        const table = document.querySelector('[class^="chartWrapper"] table');
        if (!table) return;

        const headers = [...table.querySelectorAll('thead th')].slice(1);
        if (!headers.length) return;

        const rows = [...table.querySelectorAll('tbody tr')];
        if (rows.length < 2) return;

        const results = [];

        headers.forEach((th, colIndex) => {
            const fullLabel = th.textContent.trim();
            const userMatch = fullLabel.match(/^(\S+)\s+\((.+)\)$/);
            if (!userMatch) return;

            const userName = userMatch[1];
            const statName = userMatch[2];

            // Always prefer JSON (correct on both platforms), fall back to table
            const dailyAvg = getAvgFromJson(statName, userName) ?? getAvgFromTable(rows, colIndex);
            if (dailyAvg === null) return;

            let user = results.find(r => r.userName === userName);
            if (!user) { user = { userName, stats: [] }; results.push(user); }
            user.stats.push({ statName, rawAvg: dailyAvg, display: formatAvg(statName, dailyAvg) });
        });

        if (!results.length) return;

        const statRawValues = {};
        for (const { stats } of results) {
            for (const { statName, rawAvg } of stats) {
                if (!statRawValues[statName]) statRawValues[statName] = [];
                statRawValues[statName].push(rawAvg);
            }
        }

        const statMin = {};
        const statMax = {};
        for (const [statName, values] of Object.entries(statRawValues)) {
            if (values.length < 2) continue;
            statMin[statName] = Math.min(...values);
            statMax[statName] = Math.max(...values);
        }

        document.getElementById('tspa-row')?.remove();

        const row = document.createElement('div');
        row.id = 'tspa-row';
        row.style.cssText = 'display:flex;flex-wrap:wrap;gap:12px;padding:10px 4px 6px;border-top:1px solid rgba(255,255,255,0.1);margin-top:8px;font-size:12px;line-height:1.6;';

        for (const { userName, stats } of results) {
            const card = document.createElement('div');
            card.style.cssText = 'background:#2a2a2a;border-radius:4px;padding:6px 10px;min-width:180px;';

            const heading = document.createElement('div');
            heading.textContent = userName;
            heading.style.cssText = 'font-weight:bold;color:#e8c97a;border-bottom:1px solid rgba(255,255,255,0.15);padding-bottom:3px;margin-bottom:4px;';
            card.appendChild(heading);

            for (const { statName, rawAvg, display } of stats) {
                const line = document.createElement('div');
                line.style.cssText = 'display:flex;justify-content:space-between;gap:14px;color:#ddd;';

                const l = document.createElement('span');
                l.textContent = statName;
                l.style.color = 'rgba(255,255,255,0.6)';

                const v = document.createElement('span');
                v.textContent = display;
                v.style.cssText = 'font-weight:600;white-space:nowrap;';

                if (statMin[statName] !== undefined && statMax[statName] !== undefined) {
                    v.style.color = rawAvg === statMax[statName] ? '#2eb85c' : rawAvg === statMin[statName] ? '#e55353' : '#fff';
                } else {
                    v.style.color = '#fff';
                }

                line.appendChild(l);
                line.appendChild(v);
                card.appendChild(line);
            }
            row.appendChild(card);
        }

        anchor.insertAdjacentElement('afterend', row);
    };

    const pollForChanges = () => {
        const table = document.querySelector('[class^="chartWrapper"] table');
        const headers = [...(table?.querySelectorAll('thead th') || [])].map(th => th.textContent).join('|');
        const firstRow = table?.querySelector('tbody tr:first-child')?.textContent || '';
        const lastRow = table?.querySelector('tbody tr:last-child')?.textContent || '';
        const key = headers + '||' + firstRow + '||' + lastRow;

        if (key && key !== lastPollKey) {
            lastPollKey = key;
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(injectAverages, 500);
        }
    };

    const tryStart = (attempts = 0) => {
        const table = document.querySelector('[class^="chartWrapper"] table');
        const anchor = document.querySelector('[class^="dropDowns"]');

        if (table && anchor) {
            injectAverages();
            setInterval(pollForChanges, 1000);
        } else if (attempts < 40) {
            setTimeout(() => tryStart(attempts + 1), 250);
        }
    };

    // Capture JSON; re-inject once it arrives so both platforms get accurate data
    const originalFetch = unsafeWindow.fetch;
    unsafeWindow.fetch = async (resource, init) => {
        const url = typeof resource === 'string' ? resource : (resource?.url ?? '');
        const response = await originalFetch(resource, init);
        if (url.includes('personalstats.php')) {
            response.clone().json().then(json => {
                if (json?.definitions) {
                    personalStats = json;
                    clearTimeout(debounceTimer);
                    debounceTimer = setTimeout(injectAverages, 300);
                }
            }).catch(() => {});
        }
        return response;
    };

    tryStart();

})();