Torn Vault

Two-click stock buyer for Torn City - navigate and buy max shares with floppy icon

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Torn Vault
// @namespace    torn-vault
// @version      2.0.0
// @description  Two-click stock buyer for Torn City - navigate and buy max shares with floppy icon
// @author       Qctsu
// @license      MIT
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
  'use strict';

  // --- Config ---
  let CFG = {
    mode: GM_getValue('mode', 'stock'),       // 'stock' or 'vault'
    stockId: GM_getValue('stockId', 10),       // default: Crude & Co
    stockName: GM_getValue('stockName', 'Crude & Co'),
    autoClick: GM_getValue('autoClick', false) // auto-click buy button
  };

  function saveCfg() {
    GM_setValue('mode', CFG.mode);
    GM_setValue('stockId', CFG.stockId);
    GM_setValue('stockName', CFG.stockName);
    GM_setValue('autoClick', CFG.autoClick);
  }

  // --- Stock list (from Torn API, sorted by price desc) ---
  var STOCKS = [
    { id: 2, name: 'Torn City Investments', acronym: 'TCI' },
    { id: 1, name: 'Torn & Shanghai Banking', acronym: 'TSB' },
    { id: 10, name: 'Crude & Co', acronym: 'CNC' },
    { id: 15, name: 'Feathery Hotels Group', acronym: 'FHG' },
    { id: 29, name: 'Mc Smoogle Corp', acronym: 'MCS' },
    { id: 30, name: 'Wind Lines Travel', acronym: 'WLT' },
    { id: 16, name: 'Symbiotic Ltd.', acronym: 'SYM' },
    { id: 3, name: 'Syscore MFG', acronym: 'SYS' },
    { id: 28, name: 'Evil Ducks Candy Corp', acronym: 'EVL' },
    { id: 18, name: 'Performance Ribaldry', acronym: 'PRN' },
    { id: 24, name: 'Munster Beverage Corp.', acronym: 'MUN' },
    { id: 17, name: 'Lucky Shot Casino', acronym: 'LSC' },
    { id: 26, name: 'International School TC', acronym: 'IST' },
    { id: 13, name: 'TC Media Productions', acronym: 'TCP' },
    { id: 31, name: 'Torn City Clothing', acronym: 'TCC' },
    { id: 27, name: "Big Al's Gun Shop", acronym: 'BAG' },
    { id: 4, name: 'Legal Authorities Group', acronym: 'LAG' },
    { id: 33, name: 'Herbal Releaf Co.', acronym: 'CBD' },
    { id: 7, name: 'Torn City Health Service', acronym: 'THS' },
    { id: 32, name: 'Alcoholics Synonymous', acronym: 'ASS' },
    { id: 9, name: 'The Torn City Times', acronym: 'TCT' },
    { id: 21, name: 'Empty Lunchbox Traders', acronym: 'ELT' },
    { id: 6, name: 'Grain', acronym: 'GRN' },
    { id: 20, name: 'Torn City Motors', acronym: 'TCM' },
    { id: 19, name: 'Eaglewood Mercenary', acronym: 'EWM' },
    { id: 11, name: 'Messaging Inc.', acronym: 'MSG' },
    { id: 22, name: 'Home Retail Group', acronym: 'HRG' },
    { id: 12, name: 'TC Music Industries', acronym: 'TMI' },
    { id: 5, name: 'Insured On Us', acronym: 'IOU' },
    { id: 23, name: 'Tell Group Plc.', acronym: 'TGP' },
    { id: 14, name: 'I Industries Ltd.', acronym: 'IIL' },
    { id: 25, name: 'West Side University', acronym: 'WSU' },
    { id: 34, name: 'Lo Squalo Waste', acronym: 'LOS' },
    { id: 35, name: 'PointLess', acronym: 'PTS' },
    { id: 8, name: 'Yazoo', acronym: 'YAZ' }
  ];

  // --- DOM helpers ---
  function setReactInput(input, value) {
    var nativeSet = Object.getOwnPropertyDescriptor(
      window.HTMLInputElement.prototype, 'value'
    ).set;
    nativeSet.call(input, String(value));
    input.dispatchEvent(new Event('input', { bubbles: true }));
    input.dispatchEvent(new Event('change', { bubbles: true }));
  }

  function waitFor(selector, timeout) {
    timeout = timeout || 10000;
    return new Promise(function (resolve, reject) {
      var el = document.querySelector(selector);
      if (el) { resolve(el); return; }
      var timer = setTimeout(function () {
        obs.disconnect();
        reject(new Error('timeout waiting for ' + selector));
      }, timeout);
      var obs = new MutationObserver(function () {
        var el = document.querySelector(selector);
        if (el) {
          obs.disconnect();
          clearTimeout(timer);
          resolve(el);
        }
      });
      obs.observe(document.body, { childList: true, subtree: true });
    });
  }

  // wait for "Confirm Transaction" button to appear (price update may delay it), then click
  function waitForConfirm(buyBlock) {
    return new Promise(function (resolve) {
      var attempts = 0;
      var maxAttempts = 40; // 10s max
      var interval = setInterval(function () {
        attempts++;
        var btns = buyBlock.querySelectorAll('button.buy___fcM6a');
        for (var i = 0; i < btns.length; i++) {
          if (btns[i].textContent.trim().toLowerCase().indexOf('confirm') !== -1) {
            clearInterval(interval);
            btns[i].click();
            showToast('Confirmed! Shares purchased.', 'ok');
            resolve();
            return;
          }
        }
        if (attempts >= maxAttempts) {
          clearInterval(interval);
          showToast('Confirm button did not appear. Check manually.', 'warn');
          resolve();
        }
      }, 250);
    });
  }

  // --- Settings modal ---
  function showSettings() {
    var existing = document.getElementById('tv-settings');
    if (existing) { existing.remove(); return; }

    var overlay = document.createElement('div');
    overlay.id = 'tv-settings';
    overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);z-index:99999;display:flex;align-items:center;justify-content:center';

    var box = document.createElement('div');
    box.style.cssText = 'background:#2a2a2a;border:1px solid #555;border-radius:6px;min-width:320px;max-width:400px;max-height:90vh;color:#ccc;font:13px/1.5 Arial,sans-serif;display:flex;flex-direction:column';

    var inputStyle = 'width:100%;box-sizing:border-box;padding:4px 6px;background:#1a1a1a;border:1px solid #555;color:#fff;border-radius:3px;margin-top:2px;-moz-appearance:textfield';
    var selectStyle = 'width:100%;box-sizing:border-box;padding:4px 28px 4px 6px;background:#1a1a1a;border:1px solid #555;color:#fff;border-radius:3px;margin-top:2px;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer';

    var stockOptions = STOCKS.map(function (s) {
      var sel = s.id === CFG.stockId ? ' selected' : '';
      return '<option value="' + s.id + '"' + sel + '>' + s.acronym + ' - ' + s.name + '</option>';
    }).join('');

    box.innerHTML = '<style>'
      + '#tv-settings select::-webkit-scrollbar{width:6px}'
      + '#tv-settings select::-webkit-scrollbar-track{background:#1a1a1a;border-radius:3px}'
      + '#tv-settings select::-webkit-scrollbar-thumb{background:#555;border-radius:3px}'
      + '#tv-settings .tv-select-wrap{position:relative;display:block}'
      + '#tv-settings .tv-select-wrap::after{content:"";position:absolute;right:10px;top:50%;transform:translateY(-50%);border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid #888;pointer-events:none}'
      + '</style>'
      + '<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #444">'
      + '<div style="font-size:14px;font-weight:bold;color:#fff">Torn Vault</div>'
      + '<span id="tv-close" style="cursor:pointer;color:#888;font-size:18px;line-height:1;padding:0 2px" title="Close">&times;</span></div>'
      + '<div style="padding:16px;overflow-y:auto;flex:1">'
      + '<label style="display:block;margin-bottom:10px">Mode<br>'
      + '<span class="tv-select-wrap"><select id="tv-mode" style="' + selectStyle + '">'
      + '<option value="stock"' + (CFG.mode === 'stock' ? ' selected' : '') + '>Stock Market</option>'
      + '<option value="vault"' + (CFG.mode === 'vault' ? ' selected' : '') + '>PI Vault</option>'
      + '</select></span></label>'
      + '<div id="tv-stock-section"><label style="display:block;margin-bottom:10px">Stock<br>'
      + '<span class="tv-select-wrap"><select id="tv-stock" style="' + selectStyle + '">'
      + stockOptions
      + '</select></span></label></div>'
      + '<div id="tv-vault-note" style="display:none;font-size:11px;color:#aa8;padding:6px 8px;background:#1a1a1a;border-radius:3px;border:1px solid #444;margin-bottom:10px">'
      + 'PI Vault: not yet implemented. Coming soon.</div>'
      + '<label style="display:flex;align-items:center;gap:8px;margin-bottom:10px;cursor:pointer">'
      + '<input id="tv-autoclick" type="checkbox"' + (CFG.autoClick ? ' checked' : '') + ' style="width:16px;height:16px;accent-color:#080;cursor:pointer">'
      + '<span>Instant buy<br><span style="font-size:11px;color:#888">Fills max shares, clicks Buy and confirms in one go. Off = fills amount and highlights Buy button for you to click.</span></span></label>'
      + '</div>'
      + '<div style="text-align:right;padding:12px 16px;border-top:1px solid #444">'
      + '<button id="tv-cancel" style="padding:4px 12px;margin-right:6px;background:#555;border:none;color:#ccc;border-radius:3px;cursor:pointer">Cancel</button>'
      + '<button id="tv-save" style="padding:4px 12px;background:#080;border:none;color:#fff;border-radius:3px;cursor:pointer">Save</button></div>';

    overlay.appendChild(box);
    document.body.appendChild(overlay);

    overlay.addEventListener('click', function (e) { if (e.target === overlay) overlay.remove(); });
    document.getElementById('tv-close').addEventListener('click', function () { overlay.remove(); });
    document.getElementById('tv-cancel').addEventListener('click', function () { overlay.remove(); });

    function toggleSections() {
      var isStock = document.getElementById('tv-mode').value === 'stock';
      document.getElementById('tv-stock-section').style.display = isStock ? '' : 'none';
      document.getElementById('tv-vault-note').style.display = isStock ? 'none' : '';
    }
    document.getElementById('tv-mode').addEventListener('change', toggleSections);
    toggleSections();

    document.getElementById('tv-save').addEventListener('click', function () {
      CFG.mode = document.getElementById('tv-mode').value;
      CFG.autoClick = document.getElementById('tv-autoclick').checked;
      var stockSelect = document.getElementById('tv-stock');
      CFG.stockId = parseInt(stockSelect.value, 10);
      CFG.stockName = stockSelect.options[stockSelect.selectedIndex].text;
      saveCfg();
      overlay.remove();
      updateBtnTooltip();
    });
  }

  GM_registerMenuCommand('Torn Vault Settings', showSettings);

  // --- Floppy disk button ---
  var tornVaultBtn = null;

  function updateBtnTooltip() {
    if (!tornVaultBtn) return;
    if (CFG.mode === 'stock') {
      tornVaultBtn.title = 'Buy max shares: ' + CFG.stockName + '\nRight-click for settings';
    } else {
      tornVaultBtn.title = 'Deposit to PI Vault\nRight-click for settings';
    }
  }

  function createBtn() {
    var moneyEl = document.getElementById('user-money');
    if (!moneyEl) return;
    var container = moneyEl.closest('.point-block___rQyUK');
    if (!container) return;
    if (container.querySelector('.tv-btn')) return;

    var btn = document.createElement('span');
    btn.className = 'tv-btn';
    btn.style.cssText = 'cursor:pointer;display:inline-flex;align-items:center;justify-content:center;'
      + 'margin-left:5px;vertical-align:middle;opacity:0.7;transition:opacity 0.2s';

    // floppy disk SVG
    btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
      + '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>'
      + '<polyline points="17 21 17 13 7 13 7 21"/>'
      + '<polyline points="7 3 7 8 15 8"/>'
      + '</svg>';

    btn.addEventListener('mouseenter', function () { btn.style.opacity = '1'; });
    btn.addEventListener('mouseleave', function () { btn.style.opacity = '0.7'; });

    btn.addEventListener('contextmenu', function (e) {
      e.preventDefault();
      e.stopPropagation();
      showSettings();
    });

    btn.addEventListener('click', function (e) {
      e.preventDefault();
      e.stopPropagation();
      executeAction();
    });

    container.appendChild(btn);
    tornVaultBtn = btn;
    updateBtnTooltip();
  }

  // --- Execute action ---
  function isUnavailable() {
    // hospital: icon15___
    var hospEl = document.querySelector('li[class*="icon15___"] a[aria-label*="Hospital"]');
    if (hospEl) return 'You are in hospital';
    // traveling: icon17___
    var travelEl = document.querySelector('li[class*="icon17___"] a[aria-label*="Traveling"], li[class*="icon17___"] a[aria-label*="travel"]');
    if (travelEl) return 'You are traveling';
    // abroad (landed): icon18___
    var abroadEl = document.querySelector('li[class*="icon18___"] a[aria-label*="Abroad"]');
    if (abroadEl) return 'You are abroad';
    return null;
  }

  function executeAction() {
    var blocked = isUnavailable();
    if (blocked) {
      showToast(blocked + ' - cannot trade', 'warn');
      return;
    }
    if (CFG.mode === 'vault') {
      showToast('PI Vault not yet implemented', 'warn');
      return;
    }
    if (CFG.mode === 'stock') {
      buyStock();
    }
  }

  function buyStock() {
    var stockUrl = 'https://www.torn.com/page.php?sid=stocks';
    var onStockPage = window.location.href.indexOf('sid=stocks') !== -1;

    if (onStockPage) {
      executeBuyOnPage();
    } else {
      showToast('Navigating to Stock Market...', 'info');
      window.location.href = stockUrl;
    }
  }

  async function executeBuyOnPage() {
    var stockId = CFG.stockId;
    var stockName = CFG.stockName;

    showToast('Looking for ' + stockName + '...', 'info');

    try {
      // wait for stock market to render
      await waitFor('#stockmarketroot .stock___ElSDB', 15000);

      // check for restriction message (traveling, hospital, jail, etc.)
      var restrictionMsg = document.querySelector('#stockmarketroot .manageBlock___PfiJh div, #stockmarketroot .sellBlock___A_yTW .manageBlock___PfiJh');
      if (restrictionMsg && restrictionMsg.textContent.trim()) {
        showToast(restrictionMsg.textContent.trim(), 'warn');
        return;
      }

      // find the stock row by ID
      var stockRow = document.querySelector('ul.stock___ElSDB[id="' + stockId + '"]');
      if (!stockRow) {
        showToast('Stock #' + stockId + ' not found on page', 'error');
        return;
      }

      // click the "owned" tab to expand buy/sell panel
      var ownedTab = stockRow.querySelector('li.stockOwned___eXJed');
      if (!ownedTab) {
        showToast('Cannot find owned tab for stock', 'error');
        return;
      }
      ownedTab.click();

      // wait for the buy panel to appear
      var dropdown = await waitFor('ul.stock___ElSDB[id="' + stockId + '"] + .stockDropdown___Y2X_v, ul.stock___ElSDB[id="' + stockId + '"] .stockDropdown___Y2X_v', 5000).catch(function () { return null; });

      if (!dropdown) {
        // dropdown might be a sibling or inside - try broader search
        await new Promise(function (r) { setTimeout(r, 1000); });
        dropdown = document.querySelector('.stockDropdown___Y2X_v[id="panel-ownedTab"]');
      }

      if (!dropdown) {
        showToast('Buy panel did not open. Click "Owned" tab manually.', 'warn');
        return;
      }

      // find buy input
      var buyBlock = dropdown.querySelector('.buyBlock___bIlBS');
      if (!buyBlock) {
        showToast('Cannot buy right now - buy panel unavailable', 'warn');
        return;
      }

      var buyInput = buyBlock.querySelector('input.input-money:not([type="hidden"])');
      if (!buyInput) {
        showToast('Cannot buy right now - input unavailable', 'warn');
        return;
      }

      // type max amount - Torn auto-caps to affordable shares
      setReactInput(buyInput, '999999999999');

      // brief delay for Torn to recalculate
      await new Promise(function (r) { setTimeout(r, 500); });

      // check if buy button is disabled (no money)
      var buyBtn = buyBlock.querySelector('button.buy___fcM6a');
      if (buyBtn && buyBtn.disabled) {
        showToast('Not enough money to buy any shares', 'warn');
        return;
      }

      // highlight buy button or auto-click
      var buyBtn = buyBlock.querySelector('button.buy___fcM6a');
      if (buyBtn) {
        if (CFG.autoClick) {
          showToast('Clicking buy...', 'info');
          buyBtn.click();
          await waitForConfirm(buyBlock);
        } else {
          buyBtn.style.background = '#0a0';
          buyBtn.style.color = '#fff';
          buyBtn.style.boxShadow = '0 0 8px #0f0';
          showToast('Ready! Click BUY to confirm.', 'ok');
        }
      } else {
        showToast('Input filled. Find and click the Buy button.', 'ok');
      }

    } catch (err) {
      console.error('[Torn Vault]', err);
      showToast('Error: ' + err.message, 'error');
    }
  }

  // --- Toast notifications ---
  var activeToast = null;

  function showToast(msg, type) {
    // remove previous toast immediately
    if (activeToast) {
      activeToast.remove();
      activeToast = null;
    }
    var colors = { info: '#369', ok: '#080', warn: '#a80', error: '#a00' };
    var el = document.createElement('div');
    el.style.cssText = 'position:fixed;top:60px;right:20px;z-index:100000;padding:10px 16px;border-radius:4px;'
      + 'color:#fff;font:13px/1.4 Arial,sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity 0.3s;'
      + 'background:' + (colors[type] || colors.info);
    el.textContent = msg;
    document.body.appendChild(el);
    activeToast = el;
    setTimeout(function () {
      el.style.opacity = '0';
      setTimeout(function () {
        el.remove();
        if (activeToast === el) activeToast = null;
      }, 300);
    }, 3000);
  }

  // --- Init ---
  function init() {
    createBtn();

    // re-check for button if sidebar loads lazily
    var obs = new MutationObserver(function () {
      var moneyEl = document.getElementById('user-money');
      if (moneyEl && !moneyEl.closest('.point-block___rQyUK').querySelector('.tv-btn')) createBtn();
    });
    obs.observe(document.body, { childList: true, subtree: true });
  }

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