Torn Vault

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         Torn Vault
// @namespace    torn-vault
// @version      2.0.1
// @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(selectorOrFn, timeout) {
    timeout = timeout || 10000;
    var isFn = typeof selectorOrFn === 'function';
    return new Promise(function (resolve, reject) {
      var el = isFn ? selectorOrFn() : document.querySelector(selectorOrFn);
      if (el) { resolve(el); return; }
      var timer = setTimeout(function () {
        obs.disconnect();
        reject(new Error('timeout waiting for ' + (isFn ? 'element' : selectorOrFn)));
      }, timeout);
      var obs = new MutationObserver(function () {
        var el = isFn ? selectorOrFn() : document.querySelector(selectorOrFn);
        if (el) {
          obs.disconnect();
          clearTimeout(timer);
          resolve(el);
        }
      });
      obs.observe(document.body, { childList: true, subtree: true });
    });
  }

  // wait for "Confirm Transaction" button, then click
  function waitForConfirm(container) {
    return new Promise(function (resolve) {
      var attempts = 0;
      var maxAttempts = 40; // 10s max
      var interval = setInterval(function () {
        attempts++;
        var btns = container.querySelectorAll('button');
        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 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('p[class*="point-block___"]');
    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() {
    var hospEl = document.querySelector('li[class*="icon15___"] a[aria-label*="Hospital"]');
    if (hospEl) return 'You are in hospital';
    var travelEl = document.querySelector('li[class*="icon17___"] a[aria-label*="Traveling"], li[class*="icon17___"] a[aria-label*="travel"]');
    if (travelEl) return 'You are traveling';
    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 ul[class*="stock___"]', 15000);

      // check for restriction message
      var restrictionMsg = document.querySelector('#stockmarketroot [class*="manageBlock___"] div');
      if (restrictionMsg && restrictionMsg.textContent.trim()) {
        showToast(restrictionMsg.textContent.trim(), 'warn');
        return;
      }

      // find the stock row by ID
      var stockRow = document.querySelector('ul[class*="stock___"][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[class*="stockOwned___"]');
      if (!ownedTab) {
        showToast('Cannot find owned tab for stock', 'error');
        return;
      }
      ownedTab.click();

      // wait for the buy panel to appear
      var dropdown = await waitFor(function () {
        return document.querySelector('#panel-ownedTab')
          || document.querySelector('[class*="stockDropdown___"]');
      }, 5000).catch(function () { return null; });

      if (!dropdown) {
        await new Promise(function (r) { setTimeout(r, 1500); });
        dropdown = document.querySelector('#panel-ownedTab')
          || document.querySelector('[class*="stockDropdown___"]');
      }

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

      // find buy block and input
      var buyBlock = dropdown.querySelector('[class*="buyBlock___"]') || dropdown;

      var buyInput = buyBlock.querySelector('input.input-money:not([type="hidden"])')
        || buyBlock.querySelector('input[type="text"]')
        || buyBlock.querySelector('input: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); });

      // find buy button
      var buyBtn = buyBlock.querySelector('button[class*="buy___"]')
        || dropdown.querySelector('button[class*="buy___"]');
      if (!buyBtn) {
        var allBtns = dropdown.querySelectorAll('button');
        for (var i = 0; i < allBtns.length; i++) {
          var txt = allBtns[i].textContent.trim().toLowerCase();
          if (txt === 'buy' || txt.indexOf('buy') === 0) {
            buyBtn = allBtns[i]; break;
          }
        }
      }

      if (buyBtn && buyBtn.disabled) {
        showToast('Not enough money to buy any shares', 'warn');
        return;
      }

      if (buyBtn) {
        if (CFG.autoClick) {
          showToast('Clicking buy...', 'info');
          buyBtn.click();
          await waitForConfirm(dropdown);
        } 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) {
    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) return;
      var cont = moneyEl.closest('p[class*="point-block___"]');
      if (cont && !cont.querySelector('.tv-btn')) createBtn();
    });
    obs.observe(document.body, { childList: true, subtree: true });
  }

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