Bazaar + TE Info Final (Integrated & Fixed)

Bazaar listing ( Weaver Site ) + TE Information PC VERSION

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name          Bazaar + TE Info Final (Integrated & Fixed)
// @namespace     https://weav3r.dev/
// @version       3.5.1
// @description   Bazaar listing ( Weaver Site ) + TE Information PC VERSION
// @author        WTV [3281931]
// @match         https://www.torn.com/*
// @grant         GM_xmlhttpRequest
// @grant         GM_addStyle
// @connect       weav3r.dev
// @connect       tornexchange.com
// @run-at        document-idle
// ==/UserScript==

(function() {
    'use strict';

    // Global State
    window._cachedListings = {};
    window._marketValueCache = {};
    window._currentMarketNetPrice = 0;

    // --- CSS ---
    GM_addStyle(`
        .bazaar-info-container { border: 1px solid #888; margin: 10px 0; padding: 5px; background: #222; color: #fff; border-radius: 4px; }
        .bazaar-info-header { font-weight: bold; margin-bottom: 5px; display: flex; flex-wrap: nowrap; justify-content: space-between; align-items: center; font-size: 14px; }
        .bazaar-title { flex-grow: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .best-buyer-line { font-weight: bold; margin-bottom: 5px; color: #FFA500; display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: center; gap: 5px; font-size: 14px; }
        .best-buyer-line .price-display { color: lime; font-weight: bold; white-space: nowrap; font-size: 16px; }
        .best-buyer-line .trader-link { color: #1E90FF; text-decoration: none; font-weight: bold; cursor: pointer; }
        .best-buyer-line .te-listings-link { color: #00BFFF; font-size: 14px; text-decoration: none; white-space: nowrap; margin-left: 5px; font-weight: bold; }
        .bazaar-item-id { color: #aaa; font-size: 13px; font-weight: bold; white-space: nowrap; margin-left: auto; }
        
        /* 4-Box Filter Grid */
        .filter-grid { display: flex; gap: 6px; margin: 8px 0; padding: 5px; border-top: 1px dashed #444; border-bottom: 1px dashed #444; }
        .filter-grid input { flex: 1; background: #111; border: 1px solid #666; color: #00FF00; padding: 5px; border-radius: 3px; text-align: center; font-size: 12px; min-width: 0; }
        .bazaar-reset-all-btn { background: #444; color: white; border: none; padding: 0 10px; cursor: pointer; font-weight: bold; border-radius: 3px; font-size: 16px; }

        .bazaar-market-calc { display: flex; align-items: center; gap: 10px; padding: 8px 0; margin-top: 5px; }
        .bazaar-calc-label { font-weight: bold; color: #ddd; font-size: 14px; white-space: nowrap; }
        .bazaar-net-profit { font-weight: bold; color: limegreen; font-size: 14px; white-space: nowrap; }
        
        .bazaar-card-container { display: flex; overflow-x: auto; padding: 5px; gap: 5px; min-height: 80px; }
        .bazaar-card { border: 1px solid #444; background: #222; color: #eee; padding: 10px; margin: 2px; width: 125px; flex-shrink: 0; display: flex; flex-direction: column; font-size: 15px; gap: 3px; border-radius: 4px; }
        .bazaar-card a { font-weight: bold; text-decoration: none; color: #1E90FF; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        
        .diff-text-positive { color: red; font-weight: bold; }
        .diff-text-negative { color: limegreen; font-weight: bold; }
        .diff-text-neutral { color: gold; font-weight: bold; }
        
        .bazaar-loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin: 10px auto; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    `);

    // --- UTILITIES ---
    function cleanName(rawName) {
        return rawName.replace(/View Info|Buy Item|Content (collapsed|expanded)/g, '').split('$')[0].trim();
    }

    async function fetchTornExchangeData(itemId) {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "GET", url: `https://tornexchange.com/api/best_listing?item_id=${itemId}`,
                onload: (r) => {
                    try {
                        const d = JSON.parse(r.responseText);
                        if(d && d.status === 'success') {
                            window._marketValueCache[itemId] = d.data.te_price;
                            resolve(d.data);
                        } else resolve(null);
                    } catch(e) { resolve(null); }
                }
            });
        });
    }

    // --- HIGHLIGHT & SCROLL LOGIC ---
    const checkAndScroll = () => {
        const params = new URLSearchParams(window.location.search);
        const targetId = params.get("highlightItem");
        if (!targetId || !window.location.href.includes("bazaar.php")) return false;

        const targetImg = document.querySelector(`img[src*="/images/items/${targetId}/"]`);
        if (targetImg) {
            const itemBox = targetImg.closest('li') || targetImg.closest('[class*="itemTile"]') || targetImg.parentElement;
            if (itemBox) {
                itemBox.style.setProperty("outline", "4px solid #00FF00", "important");
                itemBox.style.setProperty("outline-offset", "2px", "important");
                itemBox.style.setProperty("background", "rgba(0, 255, 0, 0.1)", "important");
                itemBox.scrollIntoView({ behavior: "smooth", block: "center" });
                return true;
            }
        }
        return false;
    };

    // --- UI GENERATION ---
    function createInfoContainer(itemName, itemId, teData) {
        const container = document.createElement('div');
        container.className = 'bazaar-info-container';
        container.dataset.itemid = itemId;

        const cleaned = cleanName(itemName);
        const marketVal = teData?.te_price ? `$${Math.round(teData.te_price).toLocaleString()}` : 'N/A';
        const encodedName = encodeURIComponent(cleaned);
        const teLink = `https://tornexchange.com/listings?model_name_contains=${encodedName}&order_by=&status=`;

        container.innerHTML = `
            <div class="bazaar-info-header">
                <span class="bazaar-title">${cleaned} (Market Value: ${marketVal})</span>
            </div>
            <div class="best-buyer-line">
                ${teData?.price ? `<span>Best Trader: <span class="price-display">$${Math.round(teData.price).toLocaleString()}</span> by <a class="trader-link" href="https://www.torn.com/profiles.php?XID=${teData.trader_id}" target="_blank">${teData.trader}</a></span>` : ''}
                <a href="${teLink}" target="_blank" class="te-listings-link">(TE Listings)</a>
                <span class="bazaar-item-id">Item #: ${itemId}</span>
            </div>
            <div class="filter-grid">
                <input type="number" class="f-min-p" placeholder="Min $">
                <input type="number" class="f-max-p" placeholder="Max $">
                <input type="number" class="f-min-q" placeholder="Min Qty">
                <input type="number" class="f-max-q" placeholder="Max Qty">
                <button class="bazaar-reset-all-btn">↺</button>
            </div>
            <div class="bazaar-market-calc">
                <span class="bazaar-calc-label">Profit Calc Sell Price:</span>
                <input type="text" placeholder="Enter Price" class="profit-calc-input" style="width:100px; background:#111; border:1px solid #666; color:#fff; padding:4px;">
                <span class="bazaar-calc-label">Net (5%):</span>
                <span class="bazaar-net-profit profit-net-display">$0</span>
            </div>
            <div class="bazaar-card-container"><div class="bazaar-loader"></div></div>
        `;

        setupListeners(container, itemId);
        return container;
    }

    function setupListeners(container, itemId) {
        container.querySelectorAll('.filter-grid input').forEach(input => {
            input.addEventListener('input', () => renderCards(itemId, container));
        });

        const calcInput = container.querySelector(`.profit-calc-input`);
        const netSpan = container.querySelector(`.profit-net-display`);
        calcInput.addEventListener('input', (e) => {
            const val = e.target.value.replace(/[^\d]/g, '');
            e.target.value = val ? Number(val).toLocaleString() : '';
            window._currentMarketNetPrice = Math.floor(parseInt(val || 0) * 0.95);
            netSpan.textContent = `$${window._currentMarketNetPrice.toLocaleString()}`;
            renderCards(itemId, container);
        });

        container.querySelector('.bazaar-reset-all-btn').addEventListener('click', () => {
            container.querySelectorAll('input').forEach(i => i.value = '');
            window._currentMarketNetPrice = 0;
            renderCards(itemId, container);
        });
    }

    function renderCards(itemId, container) {
        const listings = window._cachedListings[itemId] || [];
        const cardBox = container.querySelector('.bazaar-card-container');
        
        const minP = parseInt(container.querySelector('.f-min-p').value || 0);
        const maxP = parseInt(container.querySelector('.f-max-p').value || Infinity);
        const minQ = parseInt(container.querySelector('.f-min-q').value || 0);
        const maxQ = parseInt(container.querySelector('.f-max-q').value || Infinity);
        
        const marketVal = window._marketValueCache[itemId] || 0;
        if (!cardBox) return;
        cardBox.innerHTML = '';

        let filtered = listings.filter(l => 
            l.price >= minP && l.price <= (maxP || Infinity) &&
            l.quantity >= minQ && l.quantity <= (maxQ || Infinity)
        ).sort((a, b) => a.price - b.price);

        filtered.forEach(l => {
            const card = document.createElement('div');
            card.className = 'bazaar-card';
            const diff = marketVal ? ((l.price - marketVal) / marketVal * 100).toFixed(1) : 0;
            const diffClass = diff < -0.5 ? 'diff-text-negative' : (diff > 0.5 ? 'diff-text-positive' : 'diff-text-neutral');

            let marginHTML = '';
            if (window._currentMarketNetPrice > 0) {
                const margin = ((window._currentMarketNetPrice - l.price) / window._currentMarketNetPrice * 100).toFixed(2);
                const marginClass = margin > 0.1 ? 'diff-text-negative' : 'diff-text-positive';
                marginHTML = `<div style="font-size:14px"><b>Margin:</b> <span class="${marginClass}">${margin}%</span></div>`;
            }

            card.innerHTML = `
                <a href="https://www.torn.com/bazaar.php?userId=${l.player_id}&highlightItem=${itemId}#/" target="_blank">${l.player_name}</a>
                <div><b>Price:</b> $${Math.round(l.price).toLocaleString()}</div>
                <div style="display:flex; justify-content:space-between"><span><b>Qty:</b> ${l.quantity}</span><span class="${diffClass}">${diff > 0 ? '+' : ''}${diff}%</span></div>
                ${marginHTML}
            `;
            cardBox.appendChild(card);
        });
    }

    // --- MAIN OBSERVER ---
    const observer = new MutationObserver(() => {
        // Highlight check
        checkAndScroll();

        // Bazaar Data injection
        document.querySelectorAll('[class*="sellerListWrapper"]').forEach(async w => {
            if (w.dataset.bazaarProcessed) return;
            w.dataset.bazaarProcessed = 'true';
            
            const tile = w.closest('[class*="itemTile"]') || w.previousElementSibling;
            const btn = tile?.querySelector('button[aria-controls*="itemInfo"]');
            if (!btn) return;

            const itemId = btn.getAttribute('aria-controls').split('-').pop();
            const itemName = tile.querySelector('div').textContent.trim();
            
            const teData = await fetchTornExchangeData(itemId);
            const container = createInfoContainer(itemName, itemId, teData);
            w.insertBefore(container, w.firstChild);

            GM_xmlhttpRequest({
                method: "GET", url: `https://weav3r.dev/api/marketplace/${itemId}`,
                onload: (r) => {
                    const data = JSON.parse(r.responseText);
                    window._cachedListings[itemId] = data.listings;
                    renderCards(itemId, container);
                }
            });
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });
    setTimeout(checkAndScroll, 1000);

})();