Torn RR Tracker Lite

PDA-friendly Russian Roulette profit tracker using Torn API v2.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         Torn RR Tracker Lite
// @namespace    Torn.RRTracker
// @version      1.4
// @description  PDA-friendly Russian Roulette profit tracker using Torn API v2.
// @author       Skarr02 [3462286]
// @supportURL   https://www.torn.com/profiles.php?XID=3462286
// @match        https://www.torn.com/page.php?sid=russianRoulette*
// @match        https://www.torn.com/*sid=russianRoulette*
// @grant        GM.xmlHttpRequest
// @connect      api.torn.com
// @run-at       document-idle
// @license      Private to Skarr02 [3462286] – cannot be used or duplicated in any form
// ==/UserScript==

(function () {
    'use strict';

    const PANEL_ID = 'rr-lite-pda-panel';
    const API_KEY_STORAGE = 'rr_lite_api_key';
    const DATA_STORAGE = 'rr_lite_games';
    const COLLAPSED_STORAGE = 'rr_lite_pda_collapsed';
    const BACKFILL_KEY = 'rr_lite_backfill_choice';

    const RR_LOG_IDS = '8395,8396';
    const SYNC_INTERVAL = 30 * 1000;
    const API_LIMIT = 100;

    document.getElementById(PANEL_ID)?.remove();

    let apiKey = localStorage.getItem(API_KEY_STORAGE) || '';
    let games = JSON.parse(localStorage.getItem(DATA_STORAGE) || '[]');
    let collapsed = JSON.parse(localStorage.getItem(COLLAPSED_STORAGE) || 'true');
    let isSyncing = false;
    let isBackfilling = false;

    const panel = document.createElement('div');
    panel.id = PANEL_ID;

    panel.innerHTML = `
        <div class="rr-header">
            <button id="rr-toggle">${collapsed ? '+' : '-'}</button>
            <b>RR Profit</b>
            <button id="rr-sync">↻</button>
            <button id="rr-backfill">ALL</button>
            <button id="rr-drag">☰</button>
        </div>

        <div id="rr-mini"></div>

        <div id="rr-body">
            <div class="rr-row">
                <input id="rr-key" type="password" placeholder="Torn API v2 key">
                <button id="rr-save">Save</button>
            </div>

            <div class="rr-title">Profit</div>
            <div class="rr-stat"><span>All-time</span><span id="rr-profit-all">$0</span></div>
            <div class="rr-stat"><span>Monthly</span><span id="rr-profit-month">$0</span></div>
            <div class="rr-stat"><span>Weekly</span><span id="rr-profit-week">$0</span></div>
            <div class="rr-stat"><span>Daily</span><span id="rr-profit-day">$0</span></div>

            <div class="rr-title">Games</div>
            <div class="rr-stat"><span>Total</span><span id="rr-games-all">0</span></div>
            <div class="rr-stat"><span>Monthly</span><span id="rr-games-month">0</span></div>
            <div class="rr-stat"><span>Weekly</span><span id="rr-games-week">0</span></div>
            <div class="rr-stat"><span>Daily</span><span id="rr-games-day">0</span></div>

            <div id="rr-status">Waiting...</div>
        </div>
    `;

    document.body.appendChild(panel);

    const style = document.createElement('style');
    style.textContent = `
        #${PANEL_ID} {
            position: fixed;
            top: 86px;
            left: 8px;
            width: calc(100vw - 16px);
            max-width: 340px;
            z-index: 99999999;
            background: rgba(12,12,12,0.92);
            color: white;
            font-family: Arial, sans-serif;
            font-size: 13px;
            border: 1px solid rgba(255,255,255,0.25);
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 0 12px rgba(0,0,0,0.65);
            pointer-events: none;
        }

        #${PANEL_ID} * {
            pointer-events: auto;
            box-sizing: border-box;
        }

        #${PANEL_ID} .rr-header {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px;
            background: rgba(255,255,255,0.08);
        }

        #${PANEL_ID} .rr-header b {
            flex: 1;
        }

        #${PANEL_ID} button {
            background: rgba(255,255,255,0.12);
            color: white;
            border: 1px solid rgba(255,255,255,0.25);
            border-radius: 6px;
            padding: 5px 8px;
            font-weight: bold;
        }

        #${PANEL_ID} #rr-backfill {
            font-size: 11px;
            padding: 5px 6px;
        }

        #${PANEL_ID} #rr-drag {
            cursor: grab;
            touch-action: none;
        }

        #${PANEL_ID} #rr-body {
            padding: 8px;
        }

        #${PANEL_ID} .rr-row {
            display: flex;
            gap: 5px;
            margin-bottom: 8px;
        }

        #${PANEL_ID} #rr-key {
            flex: 1;
            min-width: 0;
            background: #050505;
            color: white;
            border: 1px solid #555;
            border-radius: 5px;
            padding: 6px;
        }

        #${PANEL_ID} .rr-title {
            font-weight: bold;
            border-top: 1px solid rgba(255,255,255,0.25);
            padding-top: 6px;
            margin-top: 6px;
        }

        #${PANEL_ID} .rr-stat {
            display: flex;
            justify-content: space-between;
            padding: 3px 0;
        }

        #${PANEL_ID} #rr-status {
            border-top: 1px solid rgba(255,255,255,0.25);
            margin-top: 6px;
            padding-top: 6px;
            font-size: 11px;
            opacity: 0.75;
        }

        #${PANEL_ID} #rr-mini {
            display: grid;
            grid-template-columns: 1fr 1fr 1fr;
            gap: 4px;
            padding: 8px;
            background: rgba(0,0,0,0.55);
            font-weight: bold;
            text-align: center;
            font-size: 12px;
        }

        #${PANEL_ID} #rr-mini span {
            background: rgba(255,255,255,0.08);
            border-radius: 6px;
            padding: 6px 3px;
            white-space: nowrap;
        }

        #${PANEL_ID}.collapsed #rr-body {
            display: none;
        }

        #${PANEL_ID} .rr-pos { color: #5cff87; }
        #${PANEL_ID} .rr-neg { color: #ff6868; }
        #${PANEL_ID} .rr-zero { color: #ddd; }
    `;
    document.head.appendChild(style);

    const keyInput = document.getElementById('rr-key');
    const statusDiv = document.getElementById('rr-status');

    keyInput.value = apiKey;

    document.getElementById('rr-save').onclick = () => {
        apiKey = keyInput.value.trim();
        localStorage.setItem(API_KEY_STORAGE, apiKey);
        setStatus('API key saved');
        syncNow();

        if (!localStorage.getItem(BACKFILL_KEY)) {
            setTimeout(() => askBackfill(), 500);
        }
    };

    document.getElementById('rr-sync').onclick = () => syncNow();

    document.getElementById('rr-backfill').onclick = () => {
        askBackfill(true);
    };

    document.getElementById('rr-toggle').onclick = () => {
        collapsed = !collapsed;
        localStorage.setItem(COLLAPSED_STORAGE, JSON.stringify(collapsed));
        render();
    };

    function setStatus(text) {
        statusDiv.textContent = text;
    }

    function buildUrl(extra = '') {
        return `https://api.torn.com/v2/user/log?log=${RR_LOG_IDS}&limit=${API_LIMIT}${extra}&key=${encodeURIComponent(apiKey)}&timestamp=${Date.now()}`;
    }

    function apiGet(url) {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: 'GET',
                url,
                timeout: 20000,
                onload: res => {
                    try {
                        const data = JSON.parse(res.responseText);
                        if (data.error) reject(data.error);
                        else resolve(data);
                    } catch {
                        reject(new Error('Bad API response'));
                    }
                },
                onerror: () => reject(new Error('API request failed')),
                ontimeout: () => reject(new Error('API timeout'))
            });
        });
    }

    function money(n) {
        const sign = n < 0 ? '-' : '';
        return `${sign}$${Math.abs(Math.round(n)).toLocaleString()}`;
    }

    function shortMoney(n) {
        const sign = n < 0 ? '-' : '';
        const abs = Math.abs(n);
        if (abs >= 1_000_000_000) return `${sign}$${(abs / 1_000_000_000).toFixed(1)}b`;
        if (abs >= 1_000_000) return `${sign}$${(abs / 1_000_000).toFixed(1)}m`;
        if (abs >= 1_000) return `${sign}$${(abs / 1_000).toFixed(1)}k`;
        return `${sign}$${abs}`;
    }

    function cls(n) {
        return n > 0 ? 'rr-pos' : n < 0 ? 'rr-neg' : 'rr-zero';
    }

    function setProfit(id, value) {
        const el = document.getElementById(id);
        el.textContent = money(value);
        el.className = cls(value);
    }

    function periodStarts() {
        const now = new Date();
        const dayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) / 1000;
        const daysSinceMonday = (now.getUTCDay() + 6) % 7;
        const weekStart = dayStart - daysSinceMonday * 86400;
        const monthStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1) / 1000;
        return { dayStart, weekStart, monthStart };
    }

    function calc(from = 0) {
        const filtered = games.filter(g => g.timestamp >= from);
        return {
            profit: filtered.reduce((s, g) => s + Number(g.profit || 0), 0),
            count: filtered.length
        };
    }

    function render() {
        panel.classList.toggle('collapsed', collapsed);
        document.getElementById('rr-toggle').textContent = collapsed ? '+' : '-';

        const { dayStart, weekStart, monthStart } = periodStarts();

        const all = calc(0);
        const month = calc(monthStart);
        const week = calc(weekStart);
        const day = calc(dayStart);

        setProfit('rr-profit-all', all.profit);
        setProfit('rr-profit-month', month.profit);
        setProfit('rr-profit-week', week.profit);
        setProfit('rr-profit-day', day.profit);

        document.getElementById('rr-games-all').textContent = all.count;
        document.getElementById('rr-games-month').textContent = month.count;
        document.getElementById('rr-games-week').textContent = week.count;
        document.getElementById('rr-games-day').textContent = day.count;

        document.getElementById('rr-mini').innerHTML = `
            <span class="${cls(all.profit)}">All ${shortMoney(all.profit)}</span>
            <span class="${cls(day.profit)}">Day ${shortMoney(day.profit)}</span>
            <span>Games ${all.count}</span>
        `;
    }

    function parseRRLog(log) {
        const title = String(log?.details?.title || '').toLowerCase();
        const pot = Number(log?.data?.pot || 0);

        if (!pot) return null;

        let profit = 0;

        if (title === 'casino russian roulette win') profit = pot;
        else if (title === 'casino russian roulette lose') profit = -pot;
        else return null;

        return {
            id: String(log.id || `${log.timestamp}_${log?.data?.game_id}_${profit}`),
            timestamp: Number(log.timestamp || Math.floor(Date.now() / 1000)),
            profit
        };
    }

    function addLogs(logs) {
        const existing = new Set(games.map(g => g.id));
        let added = 0;

        for (const log of logs) {
            const parsed = parseRRLog(log);
            if (!parsed) continue;

            if (!existing.has(parsed.id)) {
                games.push(parsed);
                existing.add(parsed.id);
                added++;
            }
        }

        games.sort((a, b) => a.timestamp - b.timestamp);
        localStorage.setItem(DATA_STORAGE, JSON.stringify(games));
        render();

        return added;
    }

    function syncNow() {
        if (isSyncing || isBackfilling) return;

        if (!apiKey) {
            setStatus('Enter API key first');
            return;
        }

        isSyncing = true;
        setStatus('Syncing...');

        apiGet(buildUrl())
            .then(data => {
                const logs = Array.isArray(data.log) ? data.log : [];
                const added = addLogs(logs);
                setStatus(`Last sync: ${new Date().toLocaleTimeString()} | Added ${added}`);
            })
            .catch(err => {
                const msg = err?.error || err?.message || 'API error';
                setStatus(`API error: ${msg}`);
            })
            .finally(() => {
                isSyncing = false;
            });
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function askBackfill(force = false) {
        if (!apiKey) {
            setStatus('Enter API key first');
            return;
        }

        if (!force && localStorage.getItem(BACKFILL_KEY)) return;

        const yes = confirm(
            'Pull all previous RR win/loss logs?\n\n' +
            'This may take a few minutes because Torn only allows 100 logs per request and this script waits 30 seconds between batches.'
        );

        if (!yes) {
            localStorage.setItem(BACKFILL_KEY, 'skipped');
            return;
        }

        localStorage.setItem(BACKFILL_KEY, 'started');
        backfillAllRRLogs();
    }

    async function backfillAllRRLogs() {
        if (!apiKey || isBackfilling) return;

        isBackfilling = true;

        let to = Math.floor(Date.now() / 1000);
        let totalAdded = 0;
        let batch = 1;

        try {
            while (true) {
                setStatus(`Backfill batch ${batch}... added ${totalAdded}`);

                const data = await apiGet(buildUrl(`&to=${to}`));
                const logs = Array.isArray(data.log) ? data.log : [];

                if (!logs.length) break;

                const added = addLogs(logs);
                totalAdded += added;

                const timestamps = logs
                    .map(l => Number(l.timestamp || 0))
                    .filter(Boolean);

                if (!timestamps.length) break;

                const oldest = Math.min(...timestamps);
                to = oldest - 1;

                if (logs.length < API_LIMIT) break;

                setStatus(`Backfill waiting 30s... added ${totalAdded}`);
                await sleep(30000);
                batch++;
            }

            localStorage.setItem(BACKFILL_KEY, 'done');
            setStatus(`Backfill done | Added ${totalAdded}`);
        } catch (err) {
            const msg = err?.error || err?.message || 'Backfill failed';
            setStatus(`Backfill error: ${msg}`);
        } finally {
            isBackfilling = false;
            render();
        }
    }

    function makeDraggable() {
        const drag = document.getElementById('rr-drag');

        let dragging = false;
        let startX = 0;
        let startY = 0;
        let startLeft = 0;
        let startTop = 0;

        function start(clientX, clientY) {
            dragging = true;
            startX = clientX;
            startY = clientY;
            startLeft = panel.offsetLeft;
            startTop = panel.offsetTop;
        }

        function move(clientX, clientY) {
            if (!dragging) return;
            panel.style.left = `${startLeft + clientX - startX}px`;
            panel.style.top = `${startTop + clientY - startY}px`;
        }

        function stop() {
            dragging = false;
        }

        drag.addEventListener('mousedown', e => {
            e.preventDefault();
            start(e.clientX, e.clientY);
        });

        window.addEventListener('mousemove', e => move(e.clientX, e.clientY));
        window.addEventListener('mouseup', stop);

        drag.addEventListener('touchstart', e => {
            const t = e.touches[0];
            if (!t) return;
            e.preventDefault();
            start(t.clientX, t.clientY);
        }, { passive: false });

        window.addEventListener('touchmove', e => {
            const t = e.touches[0];
            if (!t) return;
            move(t.clientX, t.clientY);
        }, { passive: true });

        window.addEventListener('touchend', stop);
    }

    makeDraggable();
    render();

    if (apiKey) syncNow();

    setInterval(syncNow, SYNC_INTERVAL);
})();