Torn Blackjack Helper

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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

})();