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.01
// @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) {}

   function getTornIDFromPage() {
       const tornUserInput = document.getElementById('torn-user');
       if (tornUserInput) {
           try {
               const userData = JSON.parse(tornUserInput.value);
               return userData.id;
           } catch (e) {
               console.error('Error parsing torn-user data:', e);
               return null;
           }
       }
       return null;
   }

   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 stored_torn_id = GM_getValue("AWH_TornID", "");
       const page_torn_id = getTornIDFromPage();
       const AWH_Key = GM_getValue("AWH_Key", "");

       // Update stored Torn ID if we found one on the page
       if (page_torn_id && page_torn_id !== stored_torn_id) {
           GM_setValue("AWH_TornID", page_torn_id);
       }

       // Use page Torn ID if available, fall back to stored ID
       const torn_id = page_torn_id || stored_torn_id;

       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 = getTornIDFromPage() || GM_getValue("AWH_TornID", "");

       if (!torn_id || !AWH_Key) {
           alert("Please set both your 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();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址