Torn Blackjack Helper

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();