TornPDA RR Tracker v4.8 (Auto-Hide Toggle)

RR panel with wins/losses, winrate, streak, profit — visible on menu (“Password”), mini-icons + colored compact profit when collapsed; drag & collapse always work; remembers collapsed state, position, and auto-hide preference; tracks during play.

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

// ==UserScript==
// @name         TornPDA RR Tracker v4.8 (Auto-Hide Toggle)
// @namespace    https://greasyfork.org/users/1493252
// @version      4.8
// @description  RR panel with wins/losses, winrate, streak, profit — visible on menu (“Password”), mini-icons + colored compact profit when collapsed; drag & collapse always work; remembers collapsed state, position, and auto-hide preference; tracks during play.
// @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';
  const STORAGE       = 'torn_rr_tracker_results';
  const POS_KEY       = 'rr_panelPos';
  const COLLAPSE_KEY  = 'rr_panelCollapsed';
  const AUTOHIDE_KEY  = 'rr_autoHide';
  const MAX           = 100;

  if (document.getElementById(PANEL_ID)) return;

  if (!document.body.innerText.includes('Password') &&
      !document.body.innerText.includes('POT MONEY')) {
    return setTimeout(waitUntilReady, 200);
  }

  let lastPot      = 0;
  let roundActive  = true;
  let hasTracked   = false;
  let results      = JSON.parse(localStorage.getItem(STORAGE) || '[]');
  let collapsed    = JSON.parse(localStorage.getItem(COLLAPSE_KEY) || 'false');
  let autoHide     = JSON.parse(localStorage.getItem(AUTOHIDE_KEY) || 'true');

  const panel = document.createElement('div');
  panel.id = PANEL_ID;
  Object.assign(panel.style, {
    position:      'fixed',
    top:           '12px',
    left:          '12px',
    background:    'rgba(0,0,0,0.6)',
    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',
    minWidth:      '140px'
  });
  document.body.appendChild(panel);

  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 {}

  const miniBar = document.createElement('div');
  Object.assign(miniBar.style, {
    display:  'none',
    flexWrap: 'wrap',
    gap:      '2px',
    padding:  '4px 0'
  });
  panel.appendChild(miniBar);

  const profitMini = document.createElement('div');
  Object.assign(profitMini.style, {
    display:    'none',
    fontSize:   '14px',
    fontFamily: 'monospace',
    margin:     '2px 0'
  });
  panel.appendChild(profitMini);

  const statusDiv = document.createElement('div');
  Object.assign(statusDiv.style, {
    position: 'absolute', top: '8px', left: '8px',
    width:    '20px',    height: '20px',
    fontSize: '18px',    cursor: 'pointer',
    color:    'rgba(255,255,255,0.7)'
  });
  panel.appendChild(statusDiv);

  const dragHandle = document.createElement('div');
  dragHandle.textContent = '☰';
  Object.assign(dragHandle.style, {
    position:    'absolute',
    top:         '8px',
    right:       '8px',
    width:       '20px',
    height:      '20px',
    fontSize:    '18px',
    cursor:      'move',
    color:       'rgba(255,255,255,0.7)',
    touchAction: 'none'
  });
  panel.appendChild(dragHandle);

  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');
  statsGroup.append(profitDiv, winrateDiv, streakDiv);

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

  const resetBtn = document.createElement('button');
  resetBtn.textContent = '🔄 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;
      hasTracked  = false;
      refreshAll();
    }
  };
  panel.appendChild(resetBtn);

  const autoHideBtn = document.createElement('button');
  autoHideBtn.textContent = autoHide ? 'Auto-Hide: On' : 'Auto-Hide: Off';
  Object.assign(autoHideBtn.style, {
    alignSelf:   'flex-start',
    background:  'rgba(255,255,255,0.1)',
    color:       '#fff',
    border:      'none',
    borderRadius:'6px',
    padding:     '4px 8px',
    cursor:      'pointer',
    marginTop:   '4px'
  });
  autoHideBtn.onmouseenter = () => autoHideBtn.style.background = 'rgba(255,255,255,0.2)';
  autoHideBtn.onmouseleave = () => autoHideBtn.style.background = 'rgba(255,255,255,0.1)';
  autoHideBtn.onclick = () => {
    autoHide = !autoHide;
    localStorage.setItem(AUTOHIDE_KEY, JSON.stringify(autoHide));
    autoHideBtn.textContent = autoHide ? 'Auto-Hide: On' : 'Auto-Hide: Off';
    refreshAll();
  };
  panel.appendChild(autoHideBtn);

  const saveResults   = () => localStorage.setItem(STORAGE, JSON.stringify(results));
  const saveCollapsed = () => localStorage.setItem(COLLAPSE_KEY, JSON.stringify(collapsed));
  const makeCircle    = color => {
    const el = document.createElement('span');
    Object.assign(el.style, {
      display:         'inline-block',
      width:           '12px',
      height:          '12px',
      borderRadius:    '50%',
      backgroundColor: color,
      marginRight:     '2px'
    });
    return el;
  };

  function updateStatus() {
    statusDiv.textContent = collapsed ? '▪' : (roundActive ? '►' : '▸');
  }

  function updatePanelVisibility() {
    if (!autoHide) {
      panel.style.display = 'flex';
      return;
    }
    const onMenu = document.body.innerText.includes('Password');
    panel.style.display = onMenu ? 'flex' : 'none';
  }

  function refreshAll() {
    const profit = results.reduce((sum, r) => r.result==='win' ? sum + r.bet : sum - r.bet, 0);
    const sign   = profit >= 0 ? '+' : '–';

    profitMini.textContent = `${sign}${Math.abs(profit).toLocaleString()}`;
    profitMini.style.color = profit >= 0 ? '#4CAF50' : '#E53935';

    profitDiv.textContent  = `💰 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 (const r of results) {
      if (r.result==='win') { if(l) break; w++; }
      else                  { if(w) break; l++; }
    }
    streakDiv.textContent = w?`🔥 Streak: ${w}`: l?`💀 Streak: ${l}`:'⏸️ No streak';

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

    miniBar.innerHTML = '';
    results.slice(0,10).forEach(r => miniBar.append(makeCircle(r.result==='win'? '#4CAF50':'#E53935')));

    miniBar.style.display     = collapsed ? 'flex' : 'none';
    profitMini.style.display  = collapsed ? 'block' : 'none';
    statsGroup.style.display       = collapsed ? 'none' : 'flex';
    resultsContainer.style.display = collapsed ? 'none' : 'block';
    resetBtn.style.display         = collapsed ? 'none' : 'inline-block';
    autoHideBtn.style.display      = collapsed ? 'none' : 'inline-block';

    updateStatus();
    updatePanelVisibility();
  }

  function addResult(type) {
    if (!roundActive) return;
    results.unshift({ result: type, bet: lastPot });
    if (results.length > MAX) results.pop();
    saveResults();
    roundActive = false;
    hasTracked  = true;
    refreshAll();
  }
  function scanPot() {
    document.querySelectorAll('body *').forEach(el => {
      const txt = el.innerText?.trim();
      if (txt?.includes('POT MONEY:$')) {
        const m = txt.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 txt = el.innerText?.trim();
      if (txt?.includes('You take your winnings')) addResult('win');
      if (txt?.includes('BANG! You fall down'))    addResult('lose');
    });
  }
  function scanStart() {
    if (hasTracked && document.body.innerText.includes('Waiting:')) {
      roundActive = true;
      hasTracked  = false;
      updateStatus();
    }
  }

  (function() {
    let mx, my;
    const savePos = () => localStorage.setItem(POS_KEY, JSON.stringify({
      top: panel.style.top, left: panel.style.left
    }));
    dragHandle.addEventListener('mousedown', e => {
      e.preventDefault(); mx = e.clientX; my = e.clientY;
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
      function onMove(ev) {
        const dx = ev.clientX - mx, dy = ev.clientY - my;
        mx = ev.clientX; my = ev.clientY;
        panel.style.top  = panel.offsetTop + dy + 'px';
        panel.style.left = panel.offsetLeft + dx + 'px';
      }
      function onUp() {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
        savePos();
      }
    });
    dragHandle.addEventListener('touchstart', e => {
      mx = e.touches[0].clientX; my = e.touches[0].clientY;
      document.addEventListener('touchmove', onTmove);
      document.addEventListener('touchend', onTend);
      function onTmove(ev) {
        const dx = ev.touches[0].clientX - mx, dy = ev.touches[0].clientY - my;
        mx = ev.touches[0].clientX; my = ev.touches[0].clientY;
        panel.style.top  = panel.offsetTop + dy + 'px';
        panel.style.left = panel.offsetLeft + dx + 'px';
      }
      function onTend() {
        document.removeEventListener('touchmove', onTmove);
        document.removeEventListener('touchend', onTend);
        savePos();
      }
    });
  })();

  statusDiv.addEventListener('click', () => {
    collapsed = !collapsed;
    localStorage.setItem(COLLAPSE_KEY, JSON.stringify(collapsed));
    refreshAll();
  });

  refreshAll();
  setInterval(() => {
    updatePanelVisibility();
    scanStart();
    scanPot();
    scanResult();
  }, 500);

})();