Torn Blackjack Helper

Heavy balance blur, session P&L tracker, bet multipliers. Cosmetic/QoL only.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Torn Blackjack Helper
// @namespace    https://www.torn.com/
// @version      2.0.0
// @description  Heavy balance blur, session P&L tracker, bet multipliers. Cosmetic/QoL only.
// @author       Muckduck
// @match        https://www.torn.com/loader.php?sid=blackjack*
// @match        https://www.torn.com/page.php?sid=blackjack*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ─── Persisted prefs ───────────────────────────────────────────────────────
  const KEY_HIDE_BAL = 'bj_hideBalance';
  const KEY_SHOW_PNL = 'bj_showPnl';

  let hideBalance = GM_getValue(KEY_HIDE_BAL, false);
  let showPnl     = GM_getValue(KEY_SHOW_PNL, true);

  // ─── Session P&L state (in-memory only, resets on page load) ──────────────
  let sessionPnl  = 0;
  let lastMoney   = null;

  // ─── Balance element ───────────────────────────────────────────────────────
  function getMoneyEl() {
    return document.getElementById('user-money');
  }

  function getRawMoney() {
    const el = getMoneyEl();
    if (!el) return null;
    const raw = parseInt(el.getAttribute('data-money'), 10);
    return isNaN(raw) ? null : raw;
  }

  // ─── Blur / unblur ─────────────────────────────────────────────────────────
  function applyBalanceVisibility() {
    const el = getMoneyEl();
    if (!el) return;
    if (hideBalance) {
      el.style.filter     = 'blur(12px) brightness(0.4)';
      el.style.userSelect = 'none';
      el.style.transition = 'filter 0.1s ease';
    } else {
      el.style.filter     = '';
      el.style.userSelect = '';
      el.style.transition = '';
    }
  }

  // ─── Session P&L ───────────────────────────────────────────────────────────
  function initSession() {
    const money = getRawMoney();
    if (money === null) return;
    lastMoney  = money;
    sessionPnl = 0;
    updatePnlDisplay();
  }

  function checkMoneyChange() {
    const money = getRawMoney();
    if (money === null || lastMoney === null) return;
    if (money !== lastMoney) {
      sessionPnl += (money - lastMoney);
      lastMoney = money;
      updatePnlDisplay();
    }
  }

  function formatMoney(n) {
    const abs = Math.abs(n);
    let str;
    if (abs >= 1_000_000_000)      str = (abs / 1_000_000_000).toFixed(2) + 'B';
    else if (abs >= 1_000_000)     str = (abs / 1_000_000).toFixed(2) + 'M';
    else if (abs >= 1_000)         str = (abs / 1_000).toFixed(1) + 'K';
    else                           str = abs.toLocaleString();
    return (n >= 0 ? '+$' : '-$') + str;
  }

  function updatePnlDisplay() {
    const el = document.getElementById('bj-pnl-value');
    if (!el) return;
    el.textContent = formatMoney(sessionPnl);
    el.style.color = sessionPnl > 0 ? '#2ecc71' : sessionPnl < 0 ? '#e74c3c' : '#aaaaaa';
  }

  // ─── Bet helpers ───────────────────────────────────────────────────────────
  function getCurrentBet() {
    const input = document.querySelector(
      'input.bet, input[name="bet"], input[class*="bet-input"], input[class*="betInput"], input[placeholder*="bet" i]'
    );
    if (!input) return null;
    const val = parseFloat(input.value.replace(/[^0-9.]/g, ''));
    return isNaN(val) ? null : { input, val };
  }

  function setBet(newVal) {
    const result = getCurrentBet();
    if (!result) { showToast('Bet input not found.'); return; }
    const rounded = Math.floor(newVal);

    // Use React's internal setter if available (handles framework-controlled inputs)
    const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
    if (nativeSetter && nativeSetter.set) {
      nativeSetter.set.call(result.input, rounded);
    } else {
      result.input.value = rounded;
    }

    // Fire both input and change — React needs InputEvent, legacy code needs Event
    result.input.dispatchEvent(new InputEvent('input',  { bubbles: true, cancelable: true }));
    result.input.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
    result.input.focus();
  }

  // ─── Toast ─────────────────────────────────────────────────────────────────
  function showToast(msg) {
    const t = document.createElement('div');
    t.textContent = msg;
    Object.assign(t.style, {
      position: 'fixed', bottom: '80px', right: '20px', zIndex: 99999,
      background: '#1a1a2e', color: '#e0c97f', padding: '8px 14px',
      borderRadius: '6px', fontSize: '13px', fontFamily: 'monospace',
      boxShadow: '0 2px 12px rgba(0,0,0,0.6)', opacity: '1',
      transition: 'opacity 0.4s ease', pointerEvents: 'none'
    });
    document.body.appendChild(t);
    setTimeout(() => { t.style.opacity = '0'; }, 2000);
    setTimeout(() => t.remove(), 2500);
  }

  // ─── UI helpers ────────────────────────────────────────────────────────────
  function styleBtn(el, bg) {
    Object.assign(el.style, {
      background: bg, color: '#f0e6c0',
      border: '1px solid rgba(255,255,255,0.12)',
      borderRadius: '6px', padding: '6px 12px',
      fontSize: '13px', fontFamily: 'monospace', fontWeight: '600',
      cursor: 'pointer', boxShadow: '0 2px 6px rgba(0,0,0,0.4)',
      transition: 'filter 0.15s ease', whiteSpace: 'nowrap',
    });
    el.onmouseenter = () => el.style.filter = 'brightness(1.25)';
    el.onmouseleave = () => el.style.filter = '';
  }

  // ─── P&L widget ────────────────────────────────────────────────────────────
  function buildPnlWidget() {
    const wrap = document.createElement('div');
    wrap.id = 'bj-pnl-widget';
    Object.assign(wrap.style, {
      background: 'rgba(8,8,18,0.92)',
      border: '1px solid rgba(255,255,255,0.08)',
      borderRadius: '10px',
      padding: '10px 16px',
      display: showPnl ? 'flex' : 'none',
      flexDirection: 'column',
      alignItems: 'flex-end',
      gap: '3px',
      backdropFilter: 'blur(6px)',
      minWidth: '110px',
    });

    const title = document.createElement('div');
    title.textContent = 'THIS SESSION';
    Object.assign(title.style, {
      fontSize: '9px', color: '#555', fontFamily: 'monospace',
      letterSpacing: '2px', fontWeight: '700', textTransform: 'uppercase'
    });

    const val = document.createElement('div');
    val.id = 'bj-pnl-value';
    Object.assign(val.style, {
      fontSize: '20px', fontFamily: 'monospace', fontWeight: '700',
      color: '#aaaaaa', letterSpacing: '0.5px', lineHeight: '1.2'
    });
    val.textContent = '+$0';

    const resetBtn = document.createElement('button');
    resetBtn.textContent = '↺ reset';
    Object.assign(resetBtn.style, {
      background: 'none', border: 'none', color: '#444',
      fontSize: '10px', fontFamily: 'monospace', cursor: 'pointer',
      padding: '0', marginTop: '4px', letterSpacing: '0.5px'
    });
    resetBtn.onmouseenter = () => resetBtn.style.color = '#888';
    resetBtn.onmouseleave = () => resetBtn.style.color = '#444';
    resetBtn.addEventListener('click', () => {
      sessionPnl = 0;
      lastMoney  = getRawMoney();
      updatePnlDisplay();
    });

    wrap.appendChild(title);
    wrap.appendChild(val);
    wrap.appendChild(resetBtn);
    return wrap;
  }

  // ─── Main panel ────────────────────────────────────────────────────────────
  function injectUI() {
    if (document.getElementById('bj-helper-panel')) return;

    const panel = document.createElement('div');
    panel.id = 'bj-helper-panel';
    Object.assign(panel.style, {
      position: 'fixed', bottom: '20px', right: '20px',
      zIndex: '99998', display: 'flex', flexDirection: 'column',
      gap: '8px', alignItems: 'flex-end',
    });

    // P&L widget (built first so togglePnlBtn can reference it)
    const pnlWidget = buildPnlWidget();

    // Toggle balance blur
    const toggleBalBtn = document.createElement('button');
    styleBtn(toggleBalBtn, hideBalance ? '#6b1111' : '#0f4a22');
    toggleBalBtn.textContent = hideBalance ? '💰 Show Balance' : '🙈 Hide Balance';
    toggleBalBtn.addEventListener('click', () => {
      hideBalance = !hideBalance;
      GM_setValue(KEY_HIDE_BAL, hideBalance);
      styleBtn(toggleBalBtn, hideBalance ? '#6b1111' : '#0f4a22');
      toggleBalBtn.textContent = hideBalance ? '💰 Show Balance' : '🙈 Hide Balance';
      applyBalanceVisibility();
    });

    // Toggle P&L display
    const togglePnlBtn = document.createElement('button');
    styleBtn(togglePnlBtn, showPnl ? '#4a3800' : '#222222');
    togglePnlBtn.textContent = showPnl ? '📊 Hide P&L' : '📊 Show P&L';
    togglePnlBtn.addEventListener('click', () => {
      showPnl = !showPnl;
      GM_setValue(KEY_SHOW_PNL, showPnl);
      pnlWidget.style.display = showPnl ? 'flex' : 'none';
      styleBtn(togglePnlBtn, showPnl ? '#4a3800' : '#222222');
      togglePnlBtn.textContent = showPnl ? '📊 Hide P&L' : '📊 Show P&L';
    });

    // Bet multiplier buttons
    const betRow = document.createElement('div');
    Object.assign(betRow.style, { display: 'flex', gap: '6px' });
    [['× ½', 0.5], ['× 1.5', 1.5], ['× 2', 2]].forEach(([lbl, mult]) => {
      const btn = document.createElement('button');
      btn.textContent = lbl;
      btn.title = `Multiply bet by ${mult}`;
      styleBtn(btn, '#152050');
      btn.addEventListener('click', () => {
        const r = getCurrentBet();
        if (r) setBet(Math.max(1, r.val * mult));
        else showToast('Bet input not found.');
      });
      betRow.appendChild(btn);
    });

    // Label
    const label = document.createElement('div');
    label.textContent = '🃏 BJ Helper v2';
    Object.assign(label.style, {
      fontSize: '10px', color: '#333', textAlign: 'right',
      fontFamily: 'monospace', letterSpacing: '0.5px'
    });

    panel.appendChild(pnlWidget);
    panel.appendChild(toggleBalBtn);
    panel.appendChild(togglePnlBtn);
    panel.appendChild(betRow);
    panel.appendChild(label);
    document.body.appendChild(panel);

    applyBalanceVisibility();
    initSession();
  }

  // ─── MutationObserver ──────────────────────────────────────────────────────
  const observer = new MutationObserver(() => {
    if (hideBalance) applyBalanceVisibility();
    checkMoneyChange();
  });

  // ─── Init ──────────────────────────────────────────────────────────────────
  function init() {
    injectUI();
    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributeFilter: ['data-money']
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    setTimeout(init, 1200);
  }

})();