TornPDA RR Tracker v4.0 (Position Memory + No-Pull Drag)

a RR panal that shows wins/loses,winrate,win/lose streak and total profit

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

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