Torn Bazaar Filter

Show only selected Bazaar items and display owned quantities from Museum

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Bazaar Filter
// @namespace    torn-bazaar-filter
// @version      1.3.2
// @description  Show only selected Bazaar items and display owned quantities from Museum
// @match        https://www.torn.com/bazaar*
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// ==/UserScript==
 
(function () {
    'use strict';
 
    /************************************************************
     * CONFIGURATION
     ************************************************************/
 
    const FILTER_ITEMS = [
        { id: 187, name: 'Teddy Bear' },
        { id: 186, name: 'Sheep' },
        { id: 215, name: 'Kitten' },
        { id: 261, name: 'Wolverine' },
        { id: 618, name: 'Stingray' },
        { id: 258, name: 'Jaguar' },
        { id: 273, name: 'Chamois' },
        { id: 268, name: 'Red Fox' },
        { id: 266, name: 'Nessie' },
        { id: 269, name: 'Monkey' },
        { id: 274, name: 'Panda' },
        { id: 281, name: 'Lion' },
        { id: 384, name: 'Camel' },
    ];
 
    const FILTER_SET = new Set(
        FILTER_ITEMS.map(item => item.id)
    );
 
 
    /************************************************************
     * PERSISTENT STATE
     ************************************************************/
 
    const STORAGE_MINIMIZED =
        'tbf-panel-minimized';
 
    const STORAGE_FILTER_ENABLED =
        'tbf-filter-enabled';
 
 
    let filterEnabled =
        localStorage.getItem(
            STORAGE_FILTER_ENABLED
        ) !== 'false';
 
    let panelMinimized =
        localStorage.getItem(
            STORAGE_MINIMIZED
        ) === 'true';
 
 
    /************************************************************
     * RUNTIME STATE
     ************************************************************/
 
    let panel = null;
 
    let observer = null;
 
    let filterTimeout = null;
 
    let museumIframe = null;
 
    // itemId -> { amount, name }
    const inventory = new Map();
 
 
    /************************************************************
     * INITIALIZE INVENTORY
     *
     * Every configured item starts at 0.
     * Museum results overwrite the ones actually owned.
     ************************************************************/
 
    function initializeInventory() {
 
        inventory.clear();
 
        for (const item of FILTER_ITEMS) {
 
            inventory.set(item.id, {
                amount: 0,
                name: item.name
            });
        }
    }
 
 
    /************************************************************
     * LOAD MUSEUM
     *
     * Uses a hidden iframe so Torn's own JavaScript can run.
     ************************************************************/
 
    function loadMuseumInventory() {
 
        initializeInventory();
 
        console.log(
            '[TBF] Loading Museum...'
        );
 
 
        // Remove previous iframe if one exists.
        if (museumIframe) {
 
            try {
                museumIframe.remove();
            } catch (e) {
                // Ignore
            }
 
            museumIframe = null;
        }
 
 
        const iframe =
            document.createElement('iframe');
 
        museumIframe = iframe;
 
        iframe.src =
            'https://www.torn.com/museum.php';
 
 
        Object.assign(
            iframe.style,
            {
                position: 'fixed',
                width: '1px',
                height: '1px',
                left: '-10000px',
                top: '-10000px',
                opacity: '0',
                pointerEvents: 'none',
                border: '0'
            }
        );
 
 
        document.body.appendChild(iframe);
 
 
        iframe.addEventListener(
            'load',
            function () {
 
                console.log(
                    '[TBF] Museum iframe loaded'
                );
 
 
                /*
                 * Museum content may be populated
                 * asynchronously by Torn.
                 *
                 * Check every 500ms for up to 10 seconds.
                 */
 
                let attempts = 0;
 
                const checkMuseum =
                    setInterval(
                        function () {
 
                            attempts++;
 
                            try {
 
                                const museumDocument =
                                    iframe.contentDocument ||
                                    iframe.contentWindow.document;
 
 
                                const museumItems =
                                    museumDocument.querySelectorAll(
                                        '.item-wrapper[itemid]'
                                    );
 
 
                                console.log(
                                    `[TBF] Museum check ${attempts}: ${museumItems.length} items`
                                );
 
 
                                if (
                                    museumItems.length > 0 ||
                                    attempts >= 20
                                ) {
 
                                    clearInterval(
                                        checkMuseum
                                    );
 
 
                                    processMuseum(
                                        museumDocument
                                    );
 
 
                                    /*
                                     * We don't need the iframe anymore.
                                     */
                                    setTimeout(
                                        () => {
 
                                            if (
                                                museumIframe === iframe
                                            ) {
 
                                                iframe.remove();
 
                                                museumIframe =
                                                    null;
                                            }
 
                                        },
                                        100
                                    );
                                }
 
 
                            } catch (error) {
 
                                console.error(
                                    '[TBF] Error reading Museum:',
                                    error
                                );
 
                                if (
                                    attempts >= 20
                                ) {
 
                                    clearInterval(
                                        checkMuseum
                                    );
                                }
                            }
 
                        },
                        500
                    );
            }
        );
    }
 
 
    /************************************************************
     * PROCESS MUSEUM DOM
     ************************************************************/
 
    function processMuseum(
        museumDocument
    ) {
 
        console.log(
            '[TBF] Processing Museum...'
        );
 
 
        const museumItems =
            museumDocument.querySelectorAll(
                '.item-wrapper[itemid]'
            );
 
 
        console.log(
            `[TBF] Found ${museumItems.length} Museum items`
        );
 
 
        for (
            const element of museumItems
        ) {
 
            const itemId =
                Number(
                    element.getAttribute(
                        'itemid'
                    )
                );
 
 
            /*
             * Ignore items not in our filter list.
             */
 
            if (
                !FILTER_SET.has(itemId)
            ) {
                continue;
            }
 
 
            const amountElement =
                element.querySelector(
                    '.item-amount.qty'
                );
 
 
            const nameElement =
                element.querySelector(
                    '.coll-item-header'
                );
 
 
            let amount = 0;
 
 
            if (amountElement) {
 
                amount =
                    parseInt(
                        amountElement
                            .textContent
                            .trim()
                            .replace(/,/g, ''),
                        10
                    ) || 0;
            }
 
 
            const configuredItem =
                FILTER_ITEMS.find(
                    item =>
                        item.id === itemId
                );
 
 
            const name = configuredItem
                        ? configuredItem.name
                        : `Item ${itemId}`;
 
 
            inventory.set(
                itemId,
                {
                    amount,
                    name
                }
            );
 
 
            console.log(
                `[TBF] ${name}: ${amount}`
            );
        }
 
 
        console.log(
            '[TBF] Final inventory:',
            Object.fromEntries(
                inventory
            )
        );
 
 
        /*
         * Update Bazaar.
         */
 
        updateOwnedLabels();
 
        updatePanel();
    }
 
 
    /************************************************************
     * GET ITEM ID FROM BAZAAR CARD
     ************************************************************/
 
    function getItemId(card) {
 
        const image =
            card.querySelector(
                'img.item-plate'
            );
 
 
        if (!image) {
            return null;
        }
 
 
        const src =
            image.getAttribute('src') ||
            image.getAttribute('srcset');
 
 
        if (!src) {
            return null;
        }
 
 
        /*
         * Example:
         *
         * /images/items/258/large.png
         *
         * => 258
         */
 
        const match =
            src.match(
                /\/images\/items\/(\d+)\//
            );
 
 
        return match
            ? Number(match[1])
            : null;
    }
 
 
    /************************************************************
     * FIND BAZAAR CARDS
     ************************************************************/
 
    function getBazaarCards() {
 
        return Array.from(
            document.querySelectorAll(
                '[data-testid="item-description"]'
            )
        );
    }
 
 
    /************************************************************
     * APPLY FILTER
     ************************************************************/
 
    function applyFilter() {
 
        const cards =
            getBazaarCards();
 
 
        let shown = 0;
 
        let hidden = 0;
 
 
        for (
            const card of cards
        ) {
 
            const itemId =
                getItemId(card);
 
 
            /*
             * If we can't identify it,
             * don't touch it.
             */
 
            if (
                itemId === null
            ) {
 
                card.style.display =
                    '';
 
                continue;
            }
 
 
            /*
             * Filter OFF
             */
 
            if (
                !filterEnabled
            ) {
 
                card.style.display =
                    '';
 
                removeOwnedLabel(
                    card
                );
 
                continue;
            }
 
 
            /*
             * Filter ON:
             *
             * Only configured items
             * remain visible.
             */
 
            if (
                FILTER_SET.has(itemId)
            ) {
 
                card.style.display =
                    '';
 
                addOwnedLabel(
                    card,
                    itemId
                );
 
                shown++;
 
            } else {
 
                card.style.display =
                    'none';
 
                hidden++;
            }
        }
 
 
        updatePanel(
            shown,
            hidden
        );
    }
 
 
    /************************************************************
     * ADD OWNED LABEL
     ************************************************************/
 
    function addOwnedLabel(
        card,
        itemId
    ) {
 
        let label =
            card.querySelector(
                '.tbf-owned'
            );
 
 
        if (!label) {
 
            label =
                document.createElement(
                    'div'
                );
 
            label.className =
                'tbf-owned';
 
 
            Object.assign(
                label.style,
                {
                    marginTop: '4px',
                    fontSize: '12px',
                    fontWeight: 'bold',
                    textAlign: 'center',
                    opacity: '0.9'
                }
            );
 
 
            const description =
                card.querySelector(
                    '[data-testid="description"]'
                );
 
 
            if (description) {
 
                description.appendChild(
                    label
                );
 
            } else {
 
                card.appendChild(
                    label
                );
            }
        }
 
 
        const item =
            inventory.get(
                itemId
            );
 
 
        const amount =
            item
                ? item.amount
                : 0;
 
 
        label.textContent =
            `You own: ${amount}`;
    }
 
 
    /************************************************************
     * REMOVE OWNED LABEL
     ************************************************************/
 
    function removeOwnedLabel(
        card
    ) {
 
        const label =
            card.querySelector(
                '.tbf-owned'
            );
 
 
        if (label) {
            label.remove();
        }
    }
 
 
    /************************************************************
     * UPDATE OWNED LABELS
     ************************************************************/
 
    function updateOwnedLabels() {
 
        if (!filterEnabled) {
            return;
        }
 
 
        for (
            const card of getBazaarCards()
        ) {
 
            const itemId =
                getItemId(card);
 
 
            if (
                itemId !== null &&
                FILTER_SET.has(itemId)
            ) {
 
                addOwnedLabel(
                    card,
                    itemId
                );
            }
        }
    }
 
 
    /************************************************************
     * CREATE PANEL
     ************************************************************/
 
    function createPanel() {
 
        /*
         * Don't create duplicates.
         */
 
        if (
            panel &&
            document.body.contains(panel)
        ) {
            return;
        }
 
 
        panel =
            document.createElement(
                'div'
            );
 
 
        panel.id =
            'tbf-panel';
 
 
        Object.assign(
            panel.style,
            {
                position: 'fixed',
                top: '120px',
                right: '20px',
                zIndex: '99999',
                background: 'rgba(20,20,20,0.95)',
                color: '#fff',
                padding: '10px 12px',
                borderRadius: '8px',
                boxShadow:
                    '0 2px 10px rgba(0,0,0,0.4)',
                fontFamily:
                    'Arial,sans-serif',
                fontSize: '13px',
                minWidth: '190px'
            }
        );
 
 
        document.body.appendChild(
            panel
        );
 
 
        /*
         * Restore previous state.
         */
 
        if (
            panelMinimized
        ) {
 
            renderMinimizedPanel();
 
        } else {
 
            renderFullPanel();
        }
    }
 
 
    /************************************************************
     * FULL PANEL
     ************************************************************/
 
    function renderFullPanel() {
 
        if (!panel) {
            return;
        }
 
 
        Object.assign(
            panel.style,
            {
                minWidth: '190px',
                width: '',
                height: '',
                padding: '10px 12px',
                borderRadius: '8px',
                display: 'block'
            }
        );
 
 
        panel.innerHTML = `
 
            <div style="
                display:flex;
                justify-content:space-between;
                align-items:center;
                gap:10px;
                margin-bottom:8px;
            ">
 
                <strong>
                    Bazaar Filter
                </strong>
 
 
                <div style="
                    display:flex;
                    gap:4px;
                ">
 
                    <button
                        id="tbf-refresh"
                        type="button"
                        title="Refresh Museum inventory"
                        style="
                            border:0;
                            background:transparent;
                            color:#fff;
                            cursor:pointer;
                            font-size:15px;
                            padding:2px 4px;
                        "
                    >↻</button>
 
 
                    <button
                        id="tbf-minimize"
                        type="button"
                        title="Minimize"
                        style="
                            border:0;
                            background:transparent;
                            color:#fff;
                            cursor:pointer;
                            font-size:16px;
                            padding:2px 4px;
                        "
                    >−</button>
 
                </div>
 
            </div>
 
 
            <div style="
                display:flex;
                justify-content:space-between;
                align-items:center;
                margin-bottom:8px;
            ">
 
                <span id="tbf-status">
                </span>
 
 
                <button
                    id="tbf-toggle"
                    type="button"
                    style="
                        border:0;
                        border-radius:12px;
                        padding:3px 10px;
                        cursor:pointer;
                        font-weight:bold;
                    "
                ></button>
 
            </div>
 
 
            <div id="tbf-items">
            </div>
        `;
 
 
        /*
         * Minimize
         */
 
        panel
            .querySelector(
                '#tbf-minimize'
            )
            .addEventListener(
                'click',
                minimizePanel
            );
 
 
        /*
         * Refresh
         */
 
        panel
            .querySelector(
                '#tbf-refresh'
            )
            .addEventListener(
                'click',
                loadMuseumInventory
            );
 
 
        /*
         * Toggle
         */
 
        panel
            .querySelector(
                '#tbf-toggle'
            )
            .addEventListener(
                'click',
                () => {
 
                    filterEnabled =
                        !filterEnabled;
 
 
                    localStorage.setItem(
                        STORAGE_FILTER_ENABLED,
                        filterEnabled
                    );
 
 
                    updateToggle();
 
                    applyFilter();
                }
            );
 
 
        updateToggle();
 
        updatePanel();
    }
 
 
    /************************************************************
     * MINIMIZED PANEL
     ************************************************************/
 
    function renderMinimizedPanel() {
 
        if (!panel) {
            return;
        }
 
 
        Object.assign(
            panel.style,
            {
                minWidth: '0',
                width: '32px',
                height: '32px',
                padding: '4px',
                borderRadius: '50%',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center'
            }
        );
 
 
        panel.innerHTML = `
 
            <button
                id="tbf-restore"
                type="button"
                title="Open Bazaar Filter"
                style="
                    border:0;
                    background:transparent;
                    color:#fff;
                    cursor:pointer;
                    font-size:18px;
                    font-weight:bold;
                    padding:2px 5px;
                "
            >☰</button>
        `;
 
 
        panel
            .querySelector(
                '#tbf-restore'
            )
            .addEventListener(
                'click',
                restorePanel
            );
    }
 
 
    /************************************************************
     * MINIMIZE
     ************************************************************/
 
    function minimizePanel() {
 
        if (!panel) {
            return;
        }
 
 
        panelMinimized =
            true;
 
 
        localStorage.setItem(
            STORAGE_MINIMIZED,
            'true'
        );
 
 
        renderMinimizedPanel();
    }
 
 
    /************************************************************
     * RESTORE
     ************************************************************/
 
    function restorePanel() {
 
        if (!panel) {
            return;
        }
 
 
        panelMinimized =
            false;
 
 
        localStorage.setItem(
            STORAGE_MINIMIZED,
            'false'
        );
 
 
        renderFullPanel();
 
 
        /*
         * Opening the panel refreshes
         * the Museum quantities.
         */
 
        loadMuseumInventory();
    }
 
 
    /************************************************************
     * TOGGLE BUTTON
     ************************************************************/
 
    function updateToggle() {
 
        if (!panel) {
            return;
        }
 
 
        const toggle =
            panel.querySelector(
                '#tbf-toggle'
            );
 
 
        if (!toggle) {
            return;
        }
 
 
        toggle.textContent =
            filterEnabled
                ? 'ON'
                : 'OFF';
 
 
        if (
            filterEnabled
        ) {
 
            toggle.style.background =
                '#4caf50';
 
            toggle.style.color =
                '#fff';
 
        } else {
 
            toggle.style.background =
                '#777';
 
            toggle.style.color =
                '#fff';
        }
    }
 
 
    /************************************************************
     * PANEL CONTENT
     ************************************************************/
 
    function updatePanel(
        shown = 0,
        hidden = 0
    ) {
 
        if (
            !panel ||
            panelMinimized
        ) {
            return;
        }
 
 
        const status =
            panel.querySelector(
                '#tbf-status'
            );
 
 
        const itemsContainer =
            panel.querySelector(
                '#tbf-items'
            );
 
 
        if (
            !status ||
            !itemsContainer
        ) {
            return;
        }
 
 
        if (
            !filterEnabled
        ) {
 
            status.textContent =
                'Filter disabled';
 
            itemsContainer.innerHTML =
                '';
 
            return;
        }
 
 
        status.textContent =
            `${shown} selected item${shown === 1 ? '' : 's'} shown`;
 
 
        itemsContainer.innerHTML =
            '';
 
 
        for (
            const config of FILTER_ITEMS
        ) {
 
            const item =
                inventory.get(
                    config.id
                );
 
 
            const row =
                document.createElement(
                    'div'
                );
 
 
            Object.assign(
                row.style,
                {
                    display: 'flex',
                    justifyContent:
                        'space-between',
                    gap: '15px',
                    padding: '2px 0'
                }
            );
 
 
            const name =
                item
                    ? item.name
                    : config.name;
 
 
            const amount =
                item
                    ? item.amount
                    : 0;
 
 
            row.innerHTML = `
                <span>
                    ${escapeHtml(name)}
                </span>
 
                <strong>
                    ${amount}
                </strong>
            `;
 
 
            itemsContainer.appendChild(
                row
            );
        }
    }
 
 
    /************************************************************
     * HTML ESCAPE
     ************************************************************/
 
    function escapeHtml(
        text
    ) {
 
        const div =
            document.createElement(
                'div'
            );
 
 
        div.textContent =
            text;
 
 
        return div.innerHTML;
    }
 
 
    /************************************************************
     * OBSERVER
     *
     * Torn can replace parts/all of the page when navigating.
     ************************************************************/
 
    function setupObserver() {
 
        if (observer) {
            return;
        }
 
 
        observer =
            new MutationObserver(
                function () {
 
                    clearTimeout(
                        filterTimeout
                    );
 
 
                    filterTimeout =
                        setTimeout(
                            function () {
 
                                /*
                                 * If Torn removed our panel,
                                 * recreate it.
                                 */
 
                                if (
                                    !document.body.contains(
                                        panel
                                    )
                                ) {
 
                                    panel = null;
 
                                    createPanel();
                                }
 
 
                                /*
                                 * Reapply Bazaar filter.
                                 */
 
                                applyFilter();
 
                            },
                            100
                        );
                }
            );
 
 
        observer.observe(
            document.body,
            {
                childList: true,
                subtree: true
            }
        );
    }
 
 
    /************************************************************
     * INITIALIZATION
     ************************************************************/
 
    function init() {
 
        console.log(
            '[TBF] Initializing Bazaar Filter'
        );
 
 
        createPanel();
 
 
        /*
         * Load current Museum quantities.
         */
 
        loadMuseumInventory();
 
 
        /*
         * Filter current Bazaar.
         */
 
        applyFilter();
 
 
        /*
         * Watch for Torn page changes.
         */
 
        setupObserver();
    }
 
 
    /*
     * Wait for Torn to render.
     */
 
    setTimeout(
        init,
        1000
    );
 
})();