Quick Access Vault

Adds buttons to withdraw/deposit preset values in property vault

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Quick Access Vault
// @namespace    https://torn.com/
// @version      1.20
// @description  Adds buttons to withdraw/deposit preset values in property vault
// @author       Scythe [2045424]
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @match        *://*.torn.com/properties.php*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_KEY = 'vaultPresets';
    const DEFAULT_PRESETS = '1m,5m,10m,25m,50m';
    const AMOUNT_UNITS = { k: 1e3, m: 1e6, b: 1e9 };

    const SELECTORS = {
        propertiesPage: '#properties-page-wrap',
        propertyOption: '.property-option',
        vaultWrap: 'div.vault-wrap',
        vaultInput: '.input-money-group input[type="text"]',
        moneyData: '[data-money]',
        presetContainer: '.preset-container-wrap',
        presetButtons: '.preset-buttons',
        configTray: '.config-tray'
    };

    const state = {
        amounts: GM_getValue(STORAGE_KEY, DEFAULT_PRESETS).split(','),
        isShiftPressed: false,
        configTrayOpen: false
    };

    function init() {
        injectStyles();
        setupKeyboardListeners();
        observePropertyChanges();
    }

    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .shift-active .preset-btn:hover {
                border-color: #00ffaa !important;
                box-shadow: 0 0 10px #00ffaa !important;
                transition: all 0.2s ease;
            }
            .config-tray {
                max-height: 0;
                overflow: hidden;
                transition: max-height 0.3s ease;
                padding: 0 10px;
            }
            .config-tray.open {
                max-height: 200px;
                padding: 10px;
            }
            .config-tray-content {
                padding: 0 5px;
            }
            .config-tray input {
                width: 100%;
                padding: 8px;
                margin: 5px 0;
                background: transparent;
                border: 1px solid #3d3d3d;
                color: inherit;
                border-radius: 3px;
                font-family: inherit;
                box-sizing: border-box;
            }
            .config-tray input:focus {
                outline: none;
                border-color: #555;
            }
            .config-tray-buttons {
                display: flex;
                gap: 5px;
                margin-top: 10px;
            }
            .config-tray-buttons button {
                flex: 1;
            }
        `;
        document.head.appendChild(style);
    }

    function setupKeyboardListeners() {
        document.addEventListener('keydown', handleKeyDown);
        document.addEventListener('keyup', handleKeyUp);
    }

    function handleKeyDown(e) {
        if (e.key === 'Shift' && !state.isShiftPressed) {
            state.isShiftPressed = true;
            toggleShiftClass(true);
        }

        if (!e.ctrlKey && !e.altKey && !e.metaKey) {
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
                return;
            }

            const codeMatch = e.code.match(/^Digit(\d)$/);
            if (codeMatch) {
                const digit = codeMatch[1];
                const index = digit === '0' ? 9 : parseInt(digit) - 1;

                if (index < state.amounts.length) {
                    e.preventDefault();
                    handlePresetClick(state.amounts[index], e.shiftKey);
                }
            }
        }
    }

    function handleKeyUp(e) {
        if (e.key === 'Shift') {
            state.isShiftPressed = false;
            toggleShiftClass(false);
        }
    }

    function toggleShiftClass(add) {
        const containers = document.querySelectorAll(SELECTORS.presetButtons);
        containers.forEach(el => el.classList.toggle('shift-active', add));
    }

    function observePropertyChanges() {
        const propertiesPage = document.querySelector(SELECTORS.propertiesPage);
        if (!propertiesPage) return;

        const observer = new MutationObserver(mutations => {
            const hasNewPropertyOption = mutations.some(mutation =>
                Array.from(mutation.addedNodes).some(node =>
                    node.nodeType === Node.ELEMENT_NODE &&
                    node.classList?.contains('property-option')
                )
            );

            if (hasNewPropertyOption) {
                addButtons();
            }
        });

        observer.observe(propertiesPage, { childList: true, subtree: true });
    }

    function parseAmount(amt) {
        const match = amt.toLowerCase().trim().match(/^(\d+(?:\.\d+)?)([kmb])?$/);
        if (!match) return 0;

        const value = parseFloat(match[1]);
        const multiplier = AMOUNT_UNITS[match[2]] || 1;
        return Math.floor(value * multiplier);
    }

    function getCurrentCash() {
        const moneyElem = document.querySelector(SELECTORS.moneyData);
        return moneyElem ? parseInt(moneyElem.getAttribute('data-money')) || 0 : 0;
    }

    function setVaultValue(value) {
        const vaultInputs = document.querySelectorAll(SELECTORS.vaultInput);
        vaultInputs.forEach(input => {
            input.value = value;
            input.dispatchEvent(new Event('input', { bubbles: true }));
        });
    }

    function handlePresetClick(amount, isShiftClick) {
        let targetValue = parseAmount(amount);

        if (isShiftClick) {
            const currentCash = getCurrentCash();
            targetValue = Math.max(0, targetValue - currentCash);
        }

        setVaultValue(targetValue);
    }

    function toggleConfigTray(tray) {
        state.configTrayOpen = !state.configTrayOpen;
        tray.classList.toggle('open', state.configTrayOpen);
    }

    function handleConfigureSave(inputField, tray, buttonsContainer) {
        const newPresets = inputField.value;

        if (newPresets.trim() === '') return;

        const parsedPresets = newPresets
            .split(',')
            .map(preset => preset.trim())
            .filter(preset => preset.length > 0);

        if (parsedPresets.length === 0) return;

        state.amounts = parsedPresets;
        GM_setValue(STORAGE_KEY, state.amounts.join(','));
        renderPresetButtons(buttonsContainer, tray);
        toggleConfigTray(tray);
    }

    function addButtons() {
        if (document.querySelector(SELECTORS.presetContainer)) return;

        const container = createElement('div', { class: 'preset-container-wrap' });
        const title = createElement('div', {
            class: 'title-black top-round m-top10',
            role: 'heading',
            'aria-level': '5'
        }, 'Vault Presets');

        const buttonsContainer = createElement('div', {
            class: 'preset-buttons',
            style: 'position: relative; display: flex; flex-wrap: wrap; align-items: flex-start; padding: 5px;'
        });

        const configTray = createElement('div', { class: 'config-tray' });
        const configContent = createElement('div', { class: 'config-tray-content' });

        const configInput = createElement('input', {
            type: 'text',
            placeholder: 'Enter comma-separated presets (e.g., 1m, 5m, 10m)'
        });
        configInput.value = state.amounts.join(',');

        const configButtonsDiv = createElement('div', { class: 'config-tray-buttons' });
        const saveButton = createElement('button', { class: 'torn-btn' }, 'Save');
        const cancelButton = createElement('button', { class: 'torn-btn' }, 'Cancel');

        saveButton.addEventListener('click', () => handleConfigureSave(configInput, configTray, buttonsContainer));
        cancelButton.addEventListener('click', () => {
            configInput.value = state.amounts.join(',');
            toggleConfigTray(configTray);
        });

        configButtonsDiv.append(saveButton, cancelButton);
        configContent.append(configInput, configButtonsDiv);
        configTray.appendChild(configContent);

        renderPresetButtons(buttonsContainer, configTray);

        const bottomContainer = createElement('div', { class: 'cont-gray bottom-round' });
        bottomContainer.appendChild(buttonsContainer);
        bottomContainer.appendChild(configTray);

        container.append(title, bottomContainer);

        const vaultNode = document.querySelector(SELECTORS.vaultWrap);
        if (vaultNode?.previousElementSibling) {
            vaultNode.parentNode.insertBefore(container, vaultNode.previousElementSibling);
        }
    }

    function renderPresetButtons(container, configTray) {
        const buttons = container.querySelectorAll('.preset-btn, .config-btn-main');
        buttons.forEach(btn => btn.remove());

        const fragment = document.createDocumentFragment();

        state.amounts.forEach((amount) => {
            const button = createElement('button', {
                class: 'torn-btn preset-btn',
                style: 'margin: 5px;'
            }, `$${amount}`);

            button.addEventListener('click', (e) => {
                handlePresetClick(amount, e.shiftKey);
            });

            fragment.appendChild(button);
        });

        const configButton = createElement('button', {
            class: 'torn-btn config-btn-main',
            style: 'margin-left: auto; margin-top: 5px;'
        }, 'Configure Presets');

        configButton.addEventListener('click', () => toggleConfigTray(configTray));
        fragment.appendChild(configButton);

        container.appendChild(fragment);
    }

    function createElement(type, attributes = {}, textContent = null) {
        const el = document.createElement(type);
        Object.entries(attributes).forEach(([key, value]) => {
            el.setAttribute(key, value);
        });
        if (textContent) {
            el.textContent = textContent;
        }
        return el;
    }

    init();
})();