// ==UserScript==
// @name TornPDA RR Tracker v4.0 (Position Memory + No-Pull Drag)
// @match https://www.torn.com/page.php?sid=russianRoulette*
// @grant none
// @description a RR panal that shows wins/loses,winrate,win/lose streak and total profit
// @run-at document-idle
// @version v1
// @license MIT
// @namespace https://greasyfork.org/users/1493252
// ==/UserScript==
(() => {
const STORAGE = 'torn_rr_tracker_results';
const POS_KEY = 'rr_panelPos';
const MAX = 100;
let lastPot = 0;
let roundActive = true;
let hasTrackedRound = false;
let results = JSON.parse(localStorage.getItem(STORAGE) || '[]');
let collapsed = false;
// ─── PANEL ─────────────────────────────────────────────────────────
const panel = document.createElement('div');
Object.assign(panel.style, {
position: 'fixed',
top: '12px',
left: '12px',
width: 'auto',
minWidth: '100px',
background: 'rgba(0,0,0,0.5)',
color: '#fff',
fontFamily: 'monospace',
fontSize: '14px',
padding: '36px 12px 12px 12px', // top padding for icons
borderRadius: '10px',
boxShadow: '0 0 12px rgba(255,0,0,0.3)',
zIndex: '9999999',
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
gap: '8px',
});
document.body.appendChild(panel);
// Restore saved position
try {
const pos = JSON.parse(localStorage.getItem(POS_KEY) || '{}');
if (pos.top && pos.left) {
panel.style.top = pos.top;
panel.style.left = pos.left;
}
} catch {}
// ─── ICONS ─────────────────────────────────────────────────────────
const statusDiv = document.createElement('div');
Object.assign(statusDiv.style, {
position: 'absolute',
top: '12px',
left: '12px',
width: '20px',
height: '20px',
fontSize: '18px',
lineHeight: '20px',
cursor: 'pointer',
userSelect: 'none',
color: 'rgba(255,255,255,0.6)',
});
panel.appendChild(statusDiv);
const dragHandle = document.createElement('div');
dragHandle.textContent = '☰';
Object.assign(dragHandle.style, {
position: 'absolute',
top: '12px',
right: '12px',
width: '20px',
height: '20px',
fontSize: '18px',
lineHeight: '20px',
cursor: 'move',
userSelect: 'none',
color: 'rgba(255,255,255,0.6)',
touchAction: 'none', // disable native pull-to-refresh
});
panel.appendChild(dragHandle);
// Drag & save position
(() => {
let mx, my;
const savePos = () => {
localStorage.setItem(POS_KEY, JSON.stringify({
top: panel.style.top,
left: panel.style.left
}));
};
dragHandle.onmousedown = e => {
e.preventDefault();
mx = e.clientX; my = e.clientY;
document.onmouseup = () => { document.onmousemove = document.onmouseup = null; savePos(); };
document.onmousemove = e => {
const dx = e.clientX - mx, dy = e.clientY - my;
mx = e.clientX; my = e.clientY;
panel.style.top = panel.offsetTop + dy + 'px';
panel.style.left = panel.offsetLeft + dx + 'px';
};
};
dragHandle.ontouchstart = e => {
mx = e.touches[0].clientX; my = e.touches[0].clientY;
document.ontouchend = () => { document.ontouchmove = document.ontouchend = null; savePos(); };
document.ontouchmove = e => {
const dx = e.touches[0].clientX - mx, dy = e.touches[0].clientY - my;
mx = e.touches[0].clientX; my = e.touches[0].clientY;
panel.style.top = panel.offsetTop + dy + 'px';
panel.style.left = panel.offsetLeft + dx + 'px';
};
};
})();
// Collapse/expand
statusDiv.addEventListener('click', () => {
collapsed = !collapsed;
statsGroup.style.display =
resultsContainer.style.display =
resetBtn.style.display = collapsed ? 'none' : '';
panel.style.height = collapsed ? '40px' : '';
updateStatus();
});
// ─── STATS ─────────────────────────────────────────────────────────
const statsGroup = document.createElement('div');
Object.assign(statsGroup.style, {
display: 'flex',
flexDirection: 'column',
gap: '4px',
});
panel.appendChild(statsGroup);
const profitDiv = document.createElement('div');
const winrateDiv = document.createElement('div');
const streakDiv = document.createElement('div');
[profitDiv, winrateDiv, streakDiv].forEach(d => statsGroup.appendChild(d));
// ─── RESULTS ────────────────────────────────────────────────────────
const resultsContainer = document.createElement('div');
Object.assign(resultsContainer.style, {
maxHeight: '160px',
overflowY: 'auto',
marginTop: '4px',
});
panel.appendChild(resultsContainer);
// ─── RESET BUTTON ───────────────────────────────────────────────────
const resetBtn = document.createElement('button');
resetBtn.innerHTML = '🔄 Reset';
Object.assign(resetBtn.style, {
alignSelf: 'flex-start',
background: 'rgba(255,255,255,0.1)',
color: '#fff',
border: 'none',
borderRadius:'6px',
padding: '4px 8px',
cursor: 'pointer',
userSelect: 'none',
});
resetBtn.onmouseenter = () => resetBtn.style.background = 'rgba(255,255,255,0.2)';
resetBtn.onmouseleave = () => resetBtn.style.background = 'rgba(255,255,255,0.1)';
resetBtn.onclick = () => {
if (confirm('Clear all results and reset profit?')) {
results = [];
saveResults();
lastPot = 0;
roundActive = true;
hasTrackedRound = false;
refreshAll();
}
};
panel.appendChild(resetBtn);
// ─── HELPERS ────────────────────────────────────────────────────────
const saveResults = () => localStorage.setItem(STORAGE, JSON.stringify(results));
const circle = color => {
const s = document.createElement('span');
Object.assign(s.style, {
display: 'inline-block',
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: color,
marginRight: '6px',
verticalAlign: 'middle',
});
return s;
};
function updateStatus() {
if (collapsed) {
statusDiv.textContent = '▪';
} else {
statusDiv.textContent = roundActive ? '►' : '▸';
}
}
function refreshAll() {
// Profit with sign
const profit = results.reduce((a,v) => v.result==='win'? a+v.bet : a-v.bet, 0);
const sign = profit>=0? '+' : '–';
profitDiv.textContent = `💰 Total Profit: ${sign}$${Math.abs(profit).toLocaleString()}`;
profitDiv.style.color = profit>=0? '#4CAF50':'#E53935';
// Win rate
const wins = results.filter(r=>r.result==='win').length;
const tot = results.length;
const rate = tot? ((wins/tot)*100).toFixed(1): '0.0';
winrateDiv.textContent = `🎯 Win Rate: ${rate}% (${wins}/${tot})`;
// Streak
let w=0,l=0;
for (let r of results) {
if (r.result==='win') { if (l) break; w++; }
else { if (w) break; l++; }
}
streakDiv.textContent = w? `🔥 Win Streak: ${w}`: l? `💀 Lose Streak: ${l}`:'⏸️ No streak';
// Results list
resultsContainer.innerHTML = '';
results.forEach((r,i)=>{
const row = document.createElement('div');
const dot = circle(r.result==='win'? '#4CAF50':'#E53935');
const txt = document.createElement('span');
txt.textContent = `${i+1}. ${r.result.toUpperCase()} — $${r.bet.toLocaleString()}`;
row.append(dot, txt);
resultsContainer.append(row);
});
updateStatus();
}
// ─── TRACKING ────────────────────────────────────────────────────────
function addResult(type) {
if (!roundActive) return;
results.unshift({ result:type, bet:lastPot });
if (results.length>MAX) results.pop();
saveResults();
roundActive = false;
hasTrackedRound = true;
refreshAll();
}
function scanPot() {
document.querySelectorAll('body *').forEach(el=>{
const t = el.innerText?.trim();
if (t?.includes('POT MONEY:$')) {
const m = t.match(/POT MONEY:\$\s*([\d,]+)/);
if (m) lastPot = Math.floor(parseInt(m[1].replace(/,/g,''),10)/2);
}
});
}
function scanResult() {
if (!roundActive) return;
document.querySelectorAll('body *').forEach(el=>{
const t = el.innerText?.trim();
if (t?.includes('You take your winnings')) addResult('win');
if (t?.includes('BANG! You fall down')) addResult('lose');
});
}
function scanStart() {
if (!hasTrackedRound) return;
document.querySelectorAll('body *').forEach(el=>{
if (el.innerText?.trim().includes('Waiting:')) {
roundActive = true;
hasTrackedRound = false;
updateStatus();
}
});
}
function loop() {
scanStart();
scanPot();
scanResult();
}
// ─── INIT ───────────────────────────────────────────────────────────
refreshAll();
setInterval(loop,100);
})();