Snipe Scout

Bazaar profit filter that highlights items over your custom threshold.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Snipe Scout
// @namespace    http://tampermonkey.net/
// @version      2.35
// @description  Bazaar profit filter that highlights items over your custom threshold.
// @author       Pint-Shot-Riot
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @match        https://www.torn.com/bazaar.php*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM.xmlHttpRequest
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let torn_market_values = {};
    let minProfitHighlight = parseInt(GM_getValue("MinProfitHighlight", 100000));
    let apiKey = GM_getValue("Torn_API_Key", "");
    let lastUrl = location.href;

    GM_addStyle(`
        #snipe-pill {
            position: fixed; z-index: 999999;
            background: rgba(0, 0, 0, 0.9); border: 2px solid #378c37;
            padding: 8px 16px; border-radius: 50px; color: #4fa34f;
            font-weight: bold; cursor: move; display: flex; align-items: center;
            box-shadow: 0 4px 15px rgba(0,0,0,0.5); font-family: sans-serif;
            touch-action: none; user-select: none; font-size: 11px; letter-spacing: 1px;
        }
        #snipe-menu {
            display: none; position: fixed; background: #111; border: 1px solid #333;
            padding: 15px; border-radius: 12px; z-index: 999998; width: 240px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.8); font-family: sans-serif;
        }
        .snipe-hit {
            outline: 4px solid #378c37 !important;
            outline-offset: -4px !important;
            background-color: rgba(55, 140, 55, 0.15) !important;
            box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.5) !important;
        }
        .snipe-label { color: #888; font-size: 10px; margin-top: 10px; display: block; text-transform: uppercase; }
        .snipe-input {
            width: 100%; background: #000; color: #4fa34f; border: 1px solid #333;
            padding: 8px; border-radius: 4px; margin-top: 4px; box-sizing: border-box; font-size: 12px;
        }
        .snipe-btn {
            position: relative; width: 100%; padding: 12px; margin-top: 12px; 
            background: #222; color: #eee; border: 1px solid #444; 
            border-radius: 6px; cursor: pointer; overflow: hidden; font-weight: bold;
        }
        #progress-fill {
            position: absolute; left: 0; top: 0; height: 100%;
            background: rgba(79, 163, 79, 0.3); width: 0%;
            transition: width 0.3s ease; pointer-events: none;
        }
    `);

    try {
        torn_market_values = JSON.parse(GM_getValue("Torn_Market_Values", "{}"));
    } catch (e) { torn_market_values = {}; }

    function createUI() {
        if (document.getElementById('snipe-pill')) return;
        
        const pill = document.createElement('div');
        pill.id = 'snipe-pill';
        pill.innerHTML = `SNIPE`;
        document.body.appendChild(pill);

        const menu = document.createElement('div');
        menu.id = 'snipe-menu';
        menu.innerHTML = `
            <div style="color:#4fa34f; font-size:13px; margin-bottom:5px; text-align:center; font-weight:bold; letter-spacing:0.5px;">SNIPE SCOUT</div>
            <label class="snipe-label">Torn API Key</label>
            <input type="text" id="snipe-key-input" class="snipe-input" value="${apiKey}" placeholder="Enter Key...">
            <label class="snipe-label">Min Profit Target</label>
            <input type="number" id="snipe-profit-input" class="snipe-input" value="${minProfitHighlight}">
            <button id="sync-btn" class="snipe-btn">
                <div id="progress-fill"></div>
                <span id="sync-text">SAVE & SYNC</span>
            </button>
        `;
        document.body.appendChild(menu);

        let isDragging = false, xOffset = GM_getValue("pillX", 10), yOffset = GM_getValue("pillY", 150), initialX, initialY;
        
        const updatePosition = (x, y) => {
            pill.style.left = x + "px";
            pill.style.top = y + "px";
            menu.style.left = x + "px";
            menu.style.top = (y + 40) + "px";
        };

        updatePosition(xOffset, yOffset);

        pill.onpointerdown = (e) => { 
            isDragging = true; 
            initialX = e.clientX - xOffset; 
            initialY = e.clientY - yOffset; 
            pill.setPointerCapture(e.pointerId); 
        };

        document.onpointermove = (e) => { 
            if (isDragging) { 
                xOffset = e.clientX - initialX; 
                yOffset = e.clientY - initialY; 
                updatePosition(xOffset, yOffset);
            }
        };

        pill.onpointerup = () => { 
            isDragging = false; 
            GM_setValue("pillX", xOffset); 
            GM_setValue("pillY", yOffset); 
        };

        pill.onclick = (e) => {
            if (Math.abs(e.clientX - (initialX + xOffset)) > 5) return; 
            const isVisible = menu.style.display === "block";
            menu.style.display = isVisible ? "none" : "block";
            if (!isVisible) updatePosition(xOffset, yOffset);
        };

        document.getElementById('sync-btn').onclick = () => {
            const btn = document.getElementById('sync-btn'), fill = document.getElementById('progress-fill'), text = document.getElementById('sync-text');
            apiKey = document.getElementById('snipe-key-input').value.trim();
            minProfitHighlight = parseInt(document.getElementById('snipe-profit-input').value) || 0;
            GM_setValue("Torn_API_Key", apiKey); GM_setValue("MinProfitHighlight", minProfitHighlight);
            if (!apiKey) return;
            
            btn.disabled = true; text.innerText = "SYNCING..."; fill.style.width = "20%";

            GM.xmlHttpRequest({
                method: "GET",
                url: `https://api.torn.com/torn/?key=${apiKey}&selections=items`,
                onload: (res) => {
                    const data = JSON.parse(res.responseText);
                    if (data.items) {
                        torn_market_values = Object.fromEntries(Object.entries(data.items).map(([id, item]) => [id, item.market_value || 0]));
                        GM_setValue("Torn_Market_Values", JSON.stringify(torn_market_values));
                        fill.style.width = "100%"; text.innerText = "SYNC OK!";
                        setTimeout(() => { text.innerText = "SAVE & SYNC"; fill.style.width = "0%"; btn.disabled = false; }, 2000);
                        processElements();
                    }
                }
            });
        };
    }

    function processElements() {
        const items = document.querySelectorAll('li, [class*="listItem"], [class*="row___"], [class*="sellerRow"]');
        items.forEach(container => {
            const img = container.querySelector('img[src*="/items/"], img[src*="/images/items/"]');
            if (!img) return;
            const idMatch = img.src.match(/\/(\d+)\//) || img.src.match(/\/(\d+)\.png/);
            const itemId = idMatch ? idMatch[1] : null;
            if (!itemId || !torn_market_values[itemId]) return;
            const textContent = container.innerText || "";
            const priceMatch = textContent.match(/\$[\d,]+/);
            if (!priceMatch) return;
            const currentPrice = parseInt(priceMatch[0].replace(/[^0-9]/g, ''));
            const marketValue = torn_market_values[itemId];
            if (currentPrice > 0 && (marketValue - currentPrice) >= minProfitHighlight) {
                container.classList.add('snipe-hit');
            } else {
                container.classList.remove('snipe-hit');
            }
        });
    }

    setInterval(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            processElements();
        }
    }, 500);

    const observer = new MutationObserver(() => processElements());
    observer.observe(document.body, { childList: true, subtree: true });

    createUI();
    processElements();
})();