Personal Stat Average & Comparison

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

Advertisement:

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

Advertisement:

// ==UserScript==
// @name         Personal Stat Average & Comparison
// @namespace    ben_hagen.torn.personalstataverageandcomparison
// @version      1.4.3
// @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
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  let personalStats;

  const fmt = (x) => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");

  const calcAverages = (startDate) => {
    if (!personalStats) return [];
    startDate = startDate
      ? (new Date(startDate + " 00:00:00Z")).toISOString().substring(0, 10)
      : startDate;

    // Read user/stat pairs from table headers — full untruncated names, works on all platforms
    const table = document.querySelector('[class^="chartWrapper"] table');
    if (!table) return [];
    const headers = [...table.querySelectorAll('thead th')].slice(1);
    const results = [];

    for (const th of headers) {
      const m = th.textContent.trim().match(/^(\S+)\s+\((.+)\)$/);
      if (!m) continue;
      const userName = m[1];
      const statName = m[2];

      const data = personalStats.data[
        Object.keys(personalStats.definitions).filter(key => personalStats.definitions[key] == statName)[0]
      ].filter(
        user => user.uid == Object.keys(personalStats.definitions).filter(key => personalStats.definitions[key] == userName)[0]
      )[0].data;

      const startIndex = startDate
        ? data.findIndex(e => new Date(e.time * 1e3).toISOString().substring(0, 10) == startDate)
        : data.length - 1;

      const totalDays  = (new Date(data[0].time * 1e3) - new Date(data[startIndex].time * 1e3)) / (86400 * 1e3);
      const startCount = data[startIndex].value;
      const endCount   = data[0].value;
      const average    = ((endCount - startCount) / totalDays).toFixed(2);

      let headerText;
      switch (statName) {
        case "Time played":
        case "Time spent traveling":
          headerText = `${parseInt(average / 60)} min/day`;
          break;
        case "Total networth":
        case "Rehabilitation fees":
        case "Value of received bounties":
        case "Money rewarded":
        case "Spent on bounties":
        case "Money mugged":
          headerText = `$${fmt(parseInt(average))}/day`;
          break;
        default:
          headerText = `${fmt(average)}/day`;
          break;
      }

      results.push({ userName, statName, headerText, rawAvg: parseFloat(average) });
    }
    return results;
  };

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

    let entries;
    try { entries = calcAverages(startDate); } catch(e) { return; }
    if (!entries.length) return;

    const userMap = new Map();
    for (const { userName, statName, headerText, rawAvg } of entries) {
      if (!userMap.has(userName)) userMap.set(userName, []);
      userMap.get(userName).push({ statName, headerText, rawAvg });
    }

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

    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 userMap) {
      const card = document.createElement('div');
      card.style.cssText = 'background:#2a2a2a;border-radius:4px;padding:24px 10px 6px 10px;min-width:180px;position:relative;';

      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, headerText, rawAvg } 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 = headerText;
        v.style.cssText = 'font-weight:600;white-space:nowrap;';
        if (statMin[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);
    }

    // Copy button — injected into top-right of last card after cards are built
    const copyBtn = document.createElement('button');
    copyBtn.textContent = '📋 copy all';
    copyBtn.style.cssText = 'position:absolute;top:6px;right:6px;padding:2px 7px;background:#3a3a3a;color:#ddd;border:1px solid rgba(255,255,255,0.2);border-radius:4px;cursor:pointer;font-size:10px;';
    copyBtn.addEventListener('click', () => {
      const allStats = [...new Set([...userMap.values()].flatMap(s => s.map(x => x.statName)))];
      const timePeriod = document.querySelector('.dropdown-time button[class^="toggler"]')?.textContent?.trim() || 'All';
      const header = ['User', ...allStats].join('\t');
      const lines = [...userMap.entries()].map(([userName, stats]) => {
        const cells = allStats.map(s => stats.find(x => x.statName === s)?.headerText ?? '-');
        return [userName, ...cells].join('\t');
      });
      const text = [`Period: ${timePeriod}`, header, ...lines, '', 'get this script: https://www.torn.com/forums.php#/p=threads&f=67&t=16564709'].join('\n');
      const tableRows = [
        `<tr><td colspan="99" style="padding:4px 10px;border:1px solid #ccc;background:#f0f0f0;font-style:italic;">Period: ${timePeriod}</td></tr>`,
        `<tr>${['User', ...allStats].map(h => `<th style="padding:4px 10px;border:1px solid #ccc;background:#f0f0f0;">${h}</th>`).join('')}</tr>`,
        ...[...userMap.entries()].map(([userName, stats]) => {
          const cells = allStats.map(s => stats.find(x => x.statName === s)?.headerText ?? '-');
          return `<tr>${[userName, ...cells].map(c => `<td style="padding:4px 10px;border:1px solid #ccc;">${c}</td>`).join('')}</tr>`;
        })
      ].join('');
      const html = `<table style="border-collapse:collapse;">${tableRows}<tr><td colspan="99" style="text-align:right;padding:4px 10px;border:1px solid #ccc;font-style:italic;"><a href="https://www.torn.com/forums.php#/p=threads&f=67&t=16564709" style="text-decoration:none;">get this script</a></td></tr></table>`;
      navigator.clipboard.write([new ClipboardItem({
        'text/html': new Blob([html], { type: 'text/html' }),
        'text/plain': new Blob([text], { type: 'text/plain' })
      })]).then(() => {
        copyBtn.textContent = '✓ copied!';
        setTimeout(() => { copyBtn.textContent = '📋 copy all'; }, 2000);
      });
    });

    // Place copy button in top-right corner of the last card
    const lastCard = row.lastElementChild;
    lastCard.appendChild(copyBtn);

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

  function observerFunction(mutationRecord) {
    for (const mutationEntry of mutationRecord) {
      if (mutationEntry.addedNodes) {
        for (const addedNode of mutationEntry.addedNodes) {
          if (addedNode.querySelector) {
            if (addedNode.querySelector("div > table")) injectCards();
          }
        }
      }
    }
  }

  const docObserver = new MutationObserver((mutationRecord) => {
    for (const mutationEntry of mutationRecord) {
      if (mutationEntry.addedNodes) {
        for (const addedNode of mutationEntry.addedNodes) {
          if (document.querySelector("div[class^='chartWrapper'")) {
            const target = document.querySelector("div[class^='chartWrapper'").firstElementChild;
            new MutationObserver(observerFunction).observe(target, { childList: true, subtree: true });
            docObserver.disconnect();
            return;
          }
        }
      }
    }
  });
  docObserver.observe(document, { childList: true, subtree: true });

  let lastTimePeriod = '';
  setInterval(() => {
    const btn = document.querySelector('.dropdown-time button[class^="toggler"]');
    const label = btn?.textContent?.trim() ?? '';
    if (label !== lastTimePeriod) {
      lastTimePeriod = label;
      setTimeout(injectCards, 500);
    }
  }, 500);

  const oldFetch = unsafeWindow.fetch;
  unsafeWindow.fetch = async (url, init) => {
    if (!url.includes("personalstats.php")) return oldFetch(url, init);
    let response = await oldFetch(url, init);
    let clone = response.clone();
    clone.json().then((json) => {
      if (!json.definitions) return;
      personalStats = json;
      setTimeout(injectCards, 300);
    });
    return response;
  };

})();