TornPDA RR Tracker v4.2 (Guaranteed Fix)

RR panel showing wins/losses, winrate, streak, profit — fixed for dynamic loading

当前为 2025-07-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         TornPDA RR Tracker v4.2 (Guaranteed Fix)
// @namespace    https://greasyfork.org/users/1493252
// @version      4.2
// @description  RR panel showing wins/losses, winrate, streak, profit — fixed for dynamic loading
// @match        https://www.torn.com/page.php?sid=russianRoulette*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function waitUntilReady() {
  const PANEL_ID = 'rr-tracker-panel';
  // If we've already injected, nothing more to do
  if (document.getElementById(PANEL_ID)) return;

  // Detect if the underlying Roulette area is loaded
  const rouletteLoaded = !!document.querySelector('div.roulette') 
                         || document.body.innerText.includes('POT MONEY');

  if (!rouletteLoaded) {
    // Retry after a short delay
    setTimeout(waitUntilReady, 200);
    return;
  }

  // ---- panel variables ----
  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');
  panel.id = PANEL_ID;
  Object.assign(panel.style, {
    position:      'fixed',
    top:           '12px',
    left:          '12px',
    minWidth:      '100px',
    background:    'rgba(0,0,0,0.5)',
    color:         '#fff',
    fontFamily:    'monospace',
    fontSize:      '14px',
    padding:       '36px 12px 12px',
    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 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 & CONTROLS ─────────────────
  const statusDiv = document.createElement('div');
  Object.assign(statusDiv.style, {
    position: 'absolute', top: '12px', left: '12px',
    width: '20px', height: '20px',
    fontSize: '18px', cursor: 'pointer',
    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', cursor: 'move',
    color: 'rgba(255,255,255,0.6)',
    touchAction: 'none',
  });
  panel.appendChild(dragHandle);

  // ─── Drag & Save ───────────────────────
  (function() {
    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 = null; 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 = null; 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 Toggle ───────────────────
  statusDiv.addEventListener('click', () => {
    collapsed = !collapsed;
    statsGroup.style.display = resultsContainer.style.display = resetBtn.style.display = collapsed ? 'none' : '';
    panel.style.height = collapsed ? '40px' : '';
    updateStatus();
  });

  // ─── STATS & LOG ───────────────────────
  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));

  const resultsContainer = document.createElement('div');
  Object.assign(resultsContainer.style, {
    maxHeight: '160px', overflowY: 'auto', marginTop: '4px'
  });
  panel.appendChild(resultsContainer);

  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'
  });
  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() {
    statusDiv.textContent = collapsed ? '▪' : (roundActive ? '►' : '▸');
  }

  function refreshAll() {
    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';

    const wins = results.filter(r=>r.result==='win').length;
    const tot  = results.length;
    winrateDiv.textContent = `🎯 Win Rate: ${tot?((wins/tot)*100).toFixed(1):'0.0'}% (${wins}/${tot})`;

    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';

    resultsContainer.innerHTML = '';
    results.forEach((r,i) => {
      const row = document.createElement('div');
      row.append(circle(r.result==='win'? '#4CAF50':'#E53935'),
                 document.createTextNode(`${i+1}. ${r.result.toUpperCase()} — $${r.bet.toLocaleString()}`));
      resultsContainer.appendChild(row);
    });

    updateStatus();
  }

  // ─── GAME 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, 500);
})();