Torn RR Tracker Lite

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

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

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);
})();