Torn Item Market Highlighter

Highlight items in the item market/bazaars that are at or below Arson Warehouse Pricelist and Market Value

目前為 2024-10-23 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Torn Item Market Highlighter
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Highlight items in the item market/bazaars that are at or below Arson Warehouse Pricelist and Market Value
// @author       You
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @match        https://www.torn.com/bazaar.php*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_addStyle
// @grant         GM_registerMenuCommand
// @grant         GM.xmlHttpRequest
// @require       https://kit.fontawesome.com/3054667d24.js
// ==/UserScript==

(function() {
    'use strict';

GM_addStyle(`
       .price-indicators-row {
           display: flex;
           gap: 4px;
           margin-top: 2px;
           font-size: 10px;
           justify-content: flex-end;
           width: 100%;
       }
       .sellerRow___AI0m6 .price-indicators-row {
           display: inline-flex;
           width: auto;
           margin-left: 4px;
           vertical-align: middle;
       }

       .sellerRow___AI0m6 .price___Uwiv2 {
           display: flex;
           align-items: center;
           gap: 4px;
       }
       .price-indicator {
           padding: 1px 3px;
           border-radius: 3px;
           font-weight: bold;
           white-space: nowrap;
       }
       .diff-90-100 {
           background: #004d00;
           color: white;
       }
       .diff-60-90 {
           background: #006700;
           color: white;
       }
       .diff-30-60 {
           background: #008100;
           color: white;
       }
       .diff-0-30 {
           background: #009b00;
           color: white;
       }
       .diff0-30 {
           background: #cc0000;
           color: white;
       }
       .diff30-60 {
           background: #b30000;
           color: white;
       }
       .diff60-90 {
           background: #990000;
           color: white;
       }
       .diff90-plus {
           background: #800000;
           color: white;
       }
       .diff-equal {
           background: #666666;
           color: white;
       }
   `);

    let item_prices = {};
    let torn_market_values = {};

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

    // Register menu commands for manual setup
    GM_registerMenuCommand('Add Torn Player ID', () => {
        let torn_id = GM_getValue("AWH_TornID", "");
        torn_id = prompt("Enter your Torn Player ID", torn_id);
        if (torn_id) {
            GM_setValue("AWH_TornID", torn_id);
            alert("Torn ID saved successfully!");
            checkAndUpdatePrices();
        }
    });

    GM_registerMenuCommand('Add AWH API key', () => {
        let AWH_Key = GM_getValue("AWH_Key", "");
        AWH_Key = prompt("Enter your AWH API key", AWH_Key);
        if (AWH_Key) {
            GM_setValue("AWH_Key", AWH_Key);
            alert("AWH API key saved successfully!");
            checkAndUpdatePrices();
        }
    });

    GM_registerMenuCommand('Add Torn API key', () => {
        let tornApiKey = GM_getValue("Torn_API_Key", "");
        tornApiKey = prompt("Enter your Torn API key", tornApiKey);
        if (tornApiKey) {
            GM_setValue("Torn_API_Key", tornApiKey);
            alert("Torn API key saved successfully!");
            getTornMarketValues();
        }
    });

    GM_registerMenuCommand('Get AWH Prices Now', getAWHPrices);
    GM_registerMenuCommand('Get Market Values Now', getTornMarketValues);

  function checkAndUpdatePrices() {
        const torn_id = GM_getValue("AWH_TornID", "");
        const AWH_Key = GM_getValue("AWH_Key", "");

        if (torn_id && AWH_Key) {
            getAWHPrices();
        }
    }

    function scheduleNextUpdate() {
        const now = new Date();
        const target = new Date(now);
        target.setUTCHours(20, 15, 0, 0); // 8:15 PM UTC

        if (now > target) {
            target.setDate(target.getDate() + 1);
        }

        const msUntilUpdate = target - now;
        setTimeout(() => {
            getAWHPrices();
            getTornMarketValues();
            scheduleNextUpdate();
        }, msUntilUpdate);
    }

    function getTornMarketValues() {
        const tornApiKey = GM_getValue("Torn_API_Key", "");

        if (!tornApiKey) {
            alert("Please set your Torn API key first.");
            return;
        }

        GM.xmlHttpRequest({
            method: "GET",
            url: `https://api.torn.com/torn/?key=${tornApiKey}&selections=items`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.items) {
                        Object.entries(data.items).forEach(([itemId, item]) => {
                            torn_market_values[itemId] = item.market_value || 0;
                        });
                        GM_setValue("Torn_Market_Values", JSON.stringify(torn_market_values));
                        GM_setValue("lastMarketUpdate", Date.now());
                        alert('Market values updated successfully!');
                        processElements();
                    } else {
                        alert('No market value data received. Please check your API key.');
                    }
                } catch (e) {
                    alert('Error updating market values. Please check your API key.');
                }
            },
            onerror: function() {
                alert('Failed to connect to Torn API. Please try again later.');
            }
        });
    }

    function getAWHPrices() {
        const AWH_Key = GM_getValue("AWH_Key", "");
        const torn_id = GM_getValue("AWH_TornID", "");

        if (!torn_id || !AWH_Key) {
            alert("Please set both your Torn ID and AWH API key first.");
            return;
        }

        item_prices = {};
        GM.xmlHttpRequest({
            method: "GET",
            url: `https://arsonwarehouse.com/api/v1/bids/${torn_id}`,
            headers: {
                "Authorization": "Basic " + btoa(AWH_Key + ':')
            },
            onload: function(response) {
                try {
                    const items = JSON.parse(response.responseText);
                    if (items.bids?.length > 0) {
                        items.bids.forEach(bid => {
                            if (bid.item_id && bid.bids?.length > 0) {
                                item_prices[bid.item_id] = bid.bids[0].price || 0;
                            }
                        });
                        GM_setValue("AWH_Prices", JSON.stringify(item_prices));
                        GM_setValue("lastUpdate", Date.now());
                        alert('Prices updated successfully!');
                        processElements();
                    } else {
                        alert('No price data received. Please check your credentials.');
                    }
                } catch (e) {
                    alert('Error updating prices. Please check your credentials.');
                }
            },
            onerror: function() {
                alert('Failed to connect to AWH. Please try again later.');
            }
        });
    }
function addPriceIndicator(itemId, itemPrice, container) {
    // Remove any existing indicators row
    const existingRow = container.nextElementSibling;
    if (existingRow?.classList.contains('price-indicators-row')) {
        existingRow.remove();
    }

    // Create new indicators row
    const indicatorsRow = document.createElement('div');
    indicatorsRow.classList.add('price-indicators-row');

    // Get quantity if we're in a seller row
    let quantity = 1;
    if (container.closest('.sellerRow___AI0m6')) {
        const quantityElement = container.closest('.sellerRow___AI0m6').querySelector('.available___xegv_');
        if (quantityElement) {
            // Extract number from "X available" text
            const match = quantityElement.textContent.match(/(\d+)\s+available/);
            quantity = match ? parseInt(match[1]) : 1;
        }
    }

    // AWH Price comparison (using exchange icon)
    if (item_prices[itemId]) {
        const awhPrice = item_prices[itemId];
        const awhPriceDiff = Math.round(((awhPrice - itemPrice) / awhPrice) * 100 * 100) / 100;
        const potentialProfit = (awhPrice - itemPrice) * quantity;

        const awhIndicator = document.createElement('span');
        awhIndicator.classList.add('price-indicator');
        awhIndicator.title = `Potential profit: $${potentialProfit.toLocaleString()}` +
                            (quantity > 1 ? ` (${quantity}x)` : '');
        const icon = document.createElement('i');
        icon.classList.add('fas', 'fa-exchange-alt');
        awhIndicator.appendChild(icon);
        awhIndicator.appendChild(document.createTextNode(
            ` ${awhPriceDiff > 0 ? '-' : '+'}${Math.abs(Math.round(awhPriceDiff))}%`
        ));

        if (Math.abs(awhPriceDiff) < 0.5) {
            awhIndicator.classList.add('diff-equal');
        } else if (awhPriceDiff > 0) {
            if (awhPriceDiff >= 90) awhIndicator.classList.add('diff-90-100');
            else if (awhPriceDiff >= 60) awhIndicator.classList.add('diff-60-90');
            else if (awhPriceDiff >= 30) awhIndicator.classList.add('diff-30-60');
            else awhIndicator.classList.add('diff-0-30');
        } else {
            if (awhPriceDiff <= -90) awhIndicator.classList.add('diff90-plus');
            else if (awhPriceDiff <= -60) awhIndicator.classList.add('diff60-90');
            else if (awhPriceDiff <= -30) awhIndicator.classList.add('diff30-60');
            else awhIndicator.classList.add('diff0-30');
        }

        indicatorsRow.appendChild(awhIndicator);
    }

    // Market Value comparison (using store icon)
    if (torn_market_values[itemId]) {
        const marketValue = torn_market_values[itemId];
        const marketPriceDiff = Math.round(((marketValue - itemPrice) / marketValue) * 100 * 100) / 100;
        const potentialProfit = (marketValue - itemPrice) * quantity;

        const marketIndicator = document.createElement('span');
        marketIndicator.classList.add('price-indicator');
        marketIndicator.title = `Potential profit: $${potentialProfit.toLocaleString()}` +
                               (quantity > 1 ? ` (${quantity}x)` : '');
        const icon = document.createElement('i');
        icon.classList.add('fas', 'fa-store');
        marketIndicator.appendChild(icon);
        marketIndicator.appendChild(document.createTextNode(
            ` ${marketPriceDiff > 0 ? '-' : '+'}${Math.abs(Math.round(marketPriceDiff))}%`
        ));

        if (Math.abs(marketPriceDiff) < 0.5) {
            marketIndicator.classList.add('diff-equal');
        } else if (marketPriceDiff > 0) {
            if (marketPriceDiff >= 90) marketIndicator.classList.add('diff-90-100');
            else if (marketPriceDiff >= 60) marketIndicator.classList.add('diff-60-90');
            else if (marketPriceDiff >= 30) marketIndicator.classList.add('diff-30-60');
            else marketIndicator.classList.add('diff-0-30');
        } else {
            if (marketPriceDiff <= -90) marketIndicator.classList.add('diff90-plus');
            else if (marketPriceDiff <= -60) marketIndicator.classList.add('diff60-90');
            else if (marketPriceDiff <= -30) marketIndicator.classList.add('diff30-60');
            else marketIndicator.classList.add('diff0-30');
        }

        indicatorsRow.appendChild(marketIndicator);
    }

    // Only add the row if we have at least one indicator
    if (indicatorsRow.children.length > 0) {
        container.after(indicatorsRow);
    }
}

    function updateSingleElement(element) {
        let itemId, priceElement;

        let container = element;
        while (container && !itemId) {
            const img = container.querySelector('img[src*="/images/items/"]');
            if (img) {
                const idMatch = img.src.match(/\/images\/items\/(\d+)\//);
                if (idMatch) itemId = idMatch[1];
            }
            container = container.parentElement;
        }

        if (!itemId) return;

        if (element.classList.contains('priceAndTotal___eEVS7') ||
            element.classList.contains('price___Uwiv2') ||
            element.className.includes('price_')) {
            priceElement = element;
        }

        if (priceElement) {
            const priceMatch = priceElement.textContent.match(/\$([0-9,]+)/);
            if (priceMatch) {
                const itemPrice = parseInt(priceMatch[1].replace(/,/g, ''));
                addPriceIndicator(itemId, itemPrice, priceElement);
            }
        }
    }
  function processElements() {
        if (document.URL.includes('sid=ItemMarket')) {
            document.querySelectorAll('.itemTile___cbw7w').forEach(tile => {
                const img = tile.querySelector('img.torn-item');
                if (!img) return;

                const idMatch = img.src.match(/\/images\/items\/(\d+)\//);
                if (!idMatch) return;

                const itemId = idMatch[1];
                const priceElement = tile.querySelector('.priceAndTotal___eEVS7');

                if (priceElement) {
                    const priceMatch = priceElement.textContent.match(/\$([0-9,]+)/);
                    if (priceMatch) {
                        const itemPrice = parseInt(priceMatch[1].replace(/,/g, ''));
                        addPriceIndicator(itemId, itemPrice, priceElement);
                    }
                }
            });

            document.querySelectorAll('.sellerRow___AI0m6').forEach(row => {
                const img = row.querySelector('.thumbnail___M_h9v img');
                if (!img) return;

                const idMatch = img.src.match(/\/images\/items\/(\d+)\//);
                if (!idMatch) return;

                const itemId = idMatch[1];
                const priceElement = row.querySelector('.price___Uwiv2');

                if (priceElement) {
                    const priceText = priceElement.textContent;
                    const priceMatch = priceText.match(/\$([0-9,]+)/);
                    if (priceMatch) {
                        const itemPrice = parseInt(priceMatch[1].replace(/,/g, ''));
                        addPriceIndicator(itemId, itemPrice, priceElement);
                    }
                }
            });
        }
        else if (document.URL.includes('bazaar.php')) {
            document.querySelectorAll('img[src*="/images/items/"][src*="/large.png"]').forEach(img => {
                if (!img.parentElement?.parentElement?.parentElement) return;

                const idMatch = img.src.match(/\/images\/items\/(\d+)\//);
                if (!idMatch) return;

                const itemId = idMatch[1];
                const container = img.parentElement.parentElement.parentElement;
                const priceElement = container.querySelector('[class*="price_"]');

                if (priceElement) {
                    const priceMatch = priceElement.textContent.match(/\$([0-9,]+)/);
                    if (priceMatch) {
                        const itemPrice = parseInt(priceMatch[1].replace(/,/g, ''));
                        addPriceIndicator(itemId, itemPrice, priceElement);
                    }
                }
            });
        }
    }

    function initialize() {
        const lastUpdate = GM_getValue("lastUpdate", 0);
        const lastMarketUpdate = GM_getValue("lastMarketUpdate", 0);
        const now = Date.now();

        if (now - lastUpdate > 24 * 60 * 60 * 1000) {
            getAWHPrices();
        }

        if (now - lastMarketUpdate > 24 * 60 * 60 * 1000) {
            getTornMarketValues();
        }

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

        scheduleNextUpdate();

        setTimeout(() => {
            observer.observe(document.body, {
                childList: true,
                subtree: true,
                characterData: true,
                characterDataOldValue: true,
                attributes: true,
                attributeFilter: ['class']
            });
            processElements();
        }, 1000);
    }

    const observer = new MutationObserver(mutations => {
        let affected = new Set();

        for (const mutation of mutations) {
            if (mutation.type === 'characterData') {
                let parentElement = mutation.target.parentElement;
                while (parentElement) {
                    if (parentElement.classList) {
                        if (parentElement.classList.contains('priceAndTotal___eEVS7') ||
                            parentElement.classList.contains('price___Uwiv2') ||
                            [...parentElement.classList].some(c => c.includes('price_'))) {
                            affected.add(parentElement);
                            break;
                        }
                    }
                    parentElement = parentElement.parentElement;
                }
            }
            else if (mutation.addedNodes.length) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.classList?.contains('itemTile___cbw7w') ||
                            node.classList?.contains('sellerRow___AI0m6') ||
                            node.querySelector?.('.itemTile___cbw7w, .sellerRow___AI0m6, [class*="price_"]')) {
                            processElements();
                            return;
                        }
                    }
                }
            }
        }

        affected.forEach(element => updateSingleElement(element));
    });

    initialize();

})();