PDA-friendly Russian Roulette profit tracker using Torn API v2.
// ==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)}×tamp=${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);
})();