Torn Blackjack Helper

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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

})();