Show Weapon Bonuses on Market (No API needed)

Shows the weapon bonuses for all applicable items on the item market

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Show Weapon Bonuses on Market (No API needed)
// @namespace    http://tampermonkey.net/
// @version      0.11
// @description  Shows the weapon bonuses for all applicable items on the item market
// @author       Weav3r
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @license      MIT
// @run-at       document-end
// ==/UserScript==

//////// GLOBAL VARIABLES ////////
const GLOBAL_STATE = {
  userSettings: {
    highlightItemColors: true,
  },
};

// To prevent processing the same item multiple times
const processedItems = new WeakSet();

// Cache to store already extracted bonus percentages based on bonus elements
const bonusPercentageCache = new WeakMap();

//////// VIEW ////////
const stylesheet = `
  <style>
    .enhanced-item--yellow {
      --item-bgc: rgba(252, 247, 94, 0.1);
      --item-outline: #fcf75e;
    }

    .enhanced-item--red {
      --item-bgc: rgba(255, 0, 0, 0.1);
      --item-outline: #ff0000;
    }

    .enhanced-item--orange {
      --item-bgc: rgba(255, 165, 0, 0.1);
      --item-outline: #ffa500;
    }

    .enhanced-item {
      background: var(--item-bgc);
      outline: 2px solid var(--item-outline);
      outline-offset: -2px;
      border-radius: 5px;
    }

    .bonus-row {
      display: block;
      font-size: 0.85em;
      color: #777;
      margin: 2px 0;
      line-height: 1.2;
    }

    .bonus-item {
      display: inline-block;
      margin-right: 8px;
      font-weight: bold;      /* Makes the text bold */
      font-style: italic;     /* Makes the text italic */
    }

    .bonus-item:not(:last-child)::after {
      content: '•';
      margin-left: 8px;
    }
  </style>
`;

function renderStylesheet() {
  if (!document.getElementById('item-enhancement-styles')) {
    const styleEl = document.createElement('style');
    styleEl.id = 'item-enhancement-styles';
    styleEl.textContent = stylesheet;
    document.head.appendChild(styleEl);
    console.log('Item Enhancement Stylesheet added.');
  }
}

function capitalize(text) {
  if (!text) return '';
  return text.charAt(0).toUpperCase() + text.slice(1);
}

// Helper function to simulate hover and extract percentage
function getBonusPercentage(bonusEl, bonusName) {
  return new Promise((resolve) => {
    if (!bonusEl) {
      console.warn(`Bonus element for "${bonusName}" not found.`);
      return resolve('');
    }

    // If the percentage for this bonus element is already cached, return it
    if (bonusPercentageCache.has(bonusEl)) {
      const cachedPercentage = bonusPercentageCache.get(bonusEl);
      console.log(`Percentage for "${bonusName}" fetched from cache: ${cachedPercentage}`);
      return resolve(cachedPercentage);
    }

    // Function to handle tooltip extraction
    const extractPercentage = () => {
      const tooltipId = bonusEl.getAttribute('aria-describedby');
      if (!tooltipId) {
        console.warn(`No aria-describedby found for bonus "${bonusName}".`);
        return false;
      }

      const tooltipEl = document.getElementById(tooltipId);
      if (!tooltipEl) {
        console.warn(`Tooltip element with id "${tooltipId}" not found for bonus "${bonusName}".`);
        return false;
      }

      const tooltipText = tooltipEl.innerText;
      if (!tooltipText.toLowerCase().includes(bonusName.toLowerCase())) {
        console.warn(`Tooltip text does not include the bonus name "${bonusName}".`);
        return false;
      }

      // Extract percentage using regex (e.g., "16% chance", "128% increased")
      const match = tooltipText.match(/(\d+%)/);
      if (match && match[1]) {
        const percentage = match[1];
        bonusPercentageCache.set(bonusEl, percentage); // Cache the result
        console.log(`Extracted percentage for "${bonusName}": ${percentage}`);
        resolve(percentage);
        return true;
      } else {
        console.warn(`No percentage found in tooltip for bonus "${bonusName}".`);
        resolve('');
        return true;
      }
    };

    // Simulate mouseenter to trigger tooltip
    const mouseEnterEvent = new Event('mouseenter', { bubbles: true });
    bonusEl.dispatchEvent(mouseEnterEvent);
    console.log(`Simulated mouseenter for bonus "${bonusName}".`);

    // Polling mechanism to wait for tooltip to appear and contain the correct bonus name
    const startTime = Date.now();
    const maxWait = 300; // Maximum wait time of 300ms
    const intervalTime = 10; // Check every 10ms

    const interval = setInterval(() => {
      const isExtracted = extractPercentage();
      if (isExtracted) {
        clearInterval(interval);
        // Simulate mouseleave to hide the tooltip
        const mouseLeaveEvent = new Event('mouseleave', { bubbles: true });
        bonusEl.dispatchEvent(mouseLeaveEvent);
        console.log(`Simulated mouseleave for bonus "${bonusName}".`);
      } else if (Date.now() - startTime > maxWait) {
        // Timed out
        clearInterval(interval);
        console.warn(`Timed out extracting percentage for bonus "${bonusName}".`);
        // Simulate mouseleave to hide the tooltip
        const mouseLeaveEvent = new Event('mouseleave', { bubbles: true });
        bonusEl.dispatchEvent(mouseLeaveEvent);
        resolve('');
      }
    }, intervalTime);
  });
}

//////// MAIN ////////
async function processItem(itemEl) {
  if (!itemEl || processedItems.has(itemEl)) return;

  const bonusElements = itemEl.querySelectorAll('.bonuses___a8gmz i');
  if (bonusElements.length === 0) return;

  const titleContainer = itemEl.querySelector('.title___bQI0h');
  const priceElement = titleContainer?.querySelector('.priceAndTotal___eEVS7');
  if (!titleContainer || !priceElement) return;

  // Create bonus-row only if it doesn't exist
  let bonusRow = titleContainer.querySelector('.bonus-row');
  if (!bonusRow) {
    bonusRow = document.createElement('div');
    bonusRow.className = 'bonus-row';
  } else {
    // If bonus-row exists, clear its content to update
    bonusRow.innerHTML = '';
  }

  // Process bonuses sequentially to ensure correct tooltip association
  for (const bonus of bonusElements) {
    // Extract bonus name from aria-label
    const bonusName = bonus.getAttribute('aria-label');
    if (!bonusName) {
      console.warn('Bonus element missing aria-label attribute.');
      continue;
    }

    // Get percentage by simulating hover
    const percentage = await getBonusPercentage(bonus, bonusName);

    const displayText = percentage ? `${percentage} ${bonusName}` : bonusName;

    const bonusSpan = document.createElement('span');
    bonusSpan.className = 'bonus-item';
    bonusSpan.textContent = displayText;
    bonusRow.appendChild(bonusSpan);
  }

  // Append the bonus-row to the titleContainer before the priceElement
  titleContainer.insertBefore(bonusRow, priceElement);
  console.log(`Appended bonus-row for item "${titleContainer.querySelector('.name___ukdHN').innerText}".`);

  // Add highlighting based on glow color
  if (GLOBAL_STATE.userSettings.highlightItemColors) {
    const imageWrapper = itemEl.querySelector('.imageWrapper___RqvUg');
    if (imageWrapper) {
      // Extract glow color from class (e.g., "glow-yellow-border")
      const glowClass = [...imageWrapper.classList].find(cls => cls.startsWith('glow-'));
      if (glowClass) {
        // Normalize the class by removing '-border' to extract the color
        const normalizedClass = glowClass.replace('-border', '');
        // Extract color using regex
        const colorMatch = normalizedClass.match(/glow-([a-z]+)/i);
        if (colorMatch && colorMatch[1]) {
          const color = colorMatch[1].toLowerCase();
          // Define valid colors
          const validColors = ['yellow', 'red', 'orange']; // Extend this array with more colors if needed
          if (validColors.includes(color)) {
            itemEl.classList.add('enhanced-item', `enhanced-item--${color}`);
            console.log(`Added highlighting class: enhanced-item--${color} for item "${titleContainer.querySelector('.name___ukdHN').innerText}".`);
          } else {
            console.warn(`Color "${color}" not defined in CSS. Skipping highlight for item "${titleContainer.querySelector('.name___ukdHN').innerText}".`);
          }
        } else {
          console.warn(`Failed to extract color from glow class "${normalizedClass}" for item "${titleContainer.querySelector('.name___ukdHN').innerText}".`);
        }
      } else {
        console.warn(`No glow class found on imageWrapper for item "${titleContainer.querySelector('.name___ukdHN').innerText}".`);
      }
    } else {
      console.warn(`No imageWrapper found for item "${titleContainer.querySelector('.name___ukdHN').innerText}".`);
    }
  }

  // Mark this item as processed
  processedItems.add(itemEl);
  console.log(`Processed item: "${titleContainer.querySelector('.name___ukdHN').innerText}".`);
}

async function processExistingItems() {
  const itemList = document.querySelector('.itemList___u4Hg1');
  if (!itemList) return;

  const items = itemList.querySelectorAll('.itemTile___cbw7w');
  for (const item of items) {
    await processItem(item);
  }
}

(function() {
  renderStylesheet();
  processExistingItems();

  // Set up a single MutationObserver to watch for new items anywhere in the document
  const observer = new MutationObserver(async (mutations) => {
    for (const mutation of mutations) {
      // Check if new nodes are added
      if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== Node.ELEMENT_NODE) continue;

          // If the added node is an item list, process its items
          if (node.matches('.itemList___u4Hg1')) {
            const items = node.querySelectorAll('.itemTile___cbw7w');
            for (const item of items) {
              await processItem(item);
            }
          }

          // If the added node is an individual item, process it
          if (node.matches && node.matches('.itemTile___cbw7w')) {
            await processItem(node);
          }

          // Additionally, check within the added node for any items
          const nestedItems = node.querySelectorAll('.itemTile___cbw7w');
          for (const item of nestedItems) {
            await processItem(item);
          }
        }
      }
    }
  });

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

  console.log('Item Display Enhancement script initialized.');
})();